javaSE从入门到精通的二十万字总结(一)

48 篇文章 109 订阅 ¥29.90 ¥99.00
148 篇文章 53 订阅

前言

这部分知识一共有4个文档
第二个是当前这个文档

  1. java零基础从入门到精通(全)
  2. javaSE从入门到精通的二十万字总结(一)
  3. javaSE从入门到精通的二十万字总结(二)
  4. javaSE从入门到精通的二十万字总结(三)

补充idea快捷键
快速生成main方法psvm
快速生成System.out.println(),sout
快速改写接口实现类crtl+o
删除一行ctrl + y
运行ctrl + shift + F10
任何新增/新建/添加的快捷键alt + insert
窗口变大,变小ctrl + shift + F12
切换java程序alt + 右箭头或者alt + 左箭头
切换窗口alt + 标号
提示方法的参数ctrl + p
注释单行注释:ctrl + /多行注释:ctrl + shift + /
idea当中复制一行ctrl + d

1. 面向对象

1.1 final关键字

  • final 修饰的类不能被继承
  • final 修饰的方法不能被覆盖
  • final 修饰的变量不能被修改
  • 构造方法不能被 final 修饰

代码示列:修饰的类、方法、变量
变量在没有赋值的时候可以定义可以声明
但如果输出变量就必须给予定义以及赋值

//修饰的类:
final class A1 {
	public void test1() {
	} 
}

//不能继承 A1,因为 A1 采用 final 修饰了
class B1 extends A1 {
	public void test2() {
	} 
}


//修饰的方法:
class A1 {
    public final void test1() {
    } 
}
class B1 extends A1 {
    //覆盖父类的方法,改变其行为
//因为父类的方法是 final 修饰的,所以不能覆盖
    public void test1() {
    }
    public void test2() {
    } 
}

//修饰的变量也不能修改
public class FinalTest03 {
    private static final long CARD_NO = 878778878787878L;
    public static void main(String[] args) {
//不能进行修改,因为 CARD_NO 采用 final 修改了
        CARD_NO = 99999999999999L;
    } 
}
  • 如果修饰的引用,那么这个引用只能指向一个对象,也就是说这个引用不能再次赋值,即地址已经确定了不能修改,但被指向的对象(属性值)是可以修改的

对象p不能再重新new空间,但是对象p的属性值是可以修改的。final固定的只是引用对象的地址,地址内的内容是可以修改的。

public class FinalTest05 {
    public static void main(String[] args) {
        Person p1 = new Person();
//可以赋值
        p1.name = "张三";
        System.out.println(p1.name);
        final Person p2 = new Person();
        p2.name = "李四";
        System.out.println(p2.name);
//不能编译通过
//p2 采用 final 修饰,主要限制了 p2 指向堆区中的地址不能修改(也就是
        p2 只能指向一个对象)
//p2 指向的对象的属性是可以修改的
        p2 = new Person();
    } 
}

小tips:局部变量没有赋值输出的时候有误,而实列变量的输出系统会默认赋值,但final修饰的实例变量系统不管赋值默认值,需要程序员手动赋值

class user{

//实例变量没赋值编译器出错
final int age;

//实例变量手动赋值程序不出错
final double height=1.8;

//实例变量结合构造函数初始化不出错
final double weight;
public user(){
	//不一定要在第一行
	this.weight=90;//系统默认赋值的时候是在这个时候,即赶在系统赋默认值之前就可
	this.weight=50;//在重新赋值会出错,不可以
}

实例对象存储在堆中,一个对象一份存储,既然设置final不变化,还浪费其内存,故将其设置成static静态,节省内存空间
static final 联合修饰的变量为常量,常量名件以全部大写,每个单词之间采用下划线衔接

  • 常量和静态变量一样,常量不能变化,且常量和静态变量都存在方法区,都在类加载的时候初始化
  • 调用该变量通过 类名.属性

总结:final修饰的东西都不能变即可

1.2 抽象类

抽象类是类和类之间的共同特征,将这些共同特征进一步形成抽象类,由于类本身不存在,所以抽象类无法创建对象。
类到对象是实例化,对象到类是抽象
抽象方法不能被 final 修饰,因为抽象方法就是被子类实现的

  • 采用 abstract 关键字定义的类就是抽象类,采用 abstract 关键字定义的方法就
    是抽象方法
  • 抽象的方法只需在抽象类中,提供声明,不需要实现
  • 如果一个类中含有抽象方法,那么这个类必须定义成抽象类。抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中
  • final和abstract不能同时同时使用,这两个关键字是对立的
  • 抽象类的子类可以是抽象类。也可以是非抽象类
  • 一个非抽象的类,继承抽象类,必须将抽象类中的抽象方法进行覆盖/重写/实现

抽象类无法实例化,无法创建对象,抽象类是被子类来继承的
抽象类可以有抽象类的子类

public class AbstractTest01 {
    public static void main(String[] args) {
        new a();//错误,不可创建对象
    }
}
//定义一个抽象类
abstract  class a{
    
}
//不是抽象类可以继承抽象类并且实例化
class b extends a{
}

//抽象子类可以继承抽象类
abstract class c extends a{
}

抽象类无法实例化,但抽象类有构造函数可以供子类使用
如果抽象类定义了有参构造函数而没有无参构造函数,子类定义找不到无参构造函数会出错,抽象类无法实例化调用对象(new对象),但是有构造函数可以给子类使用

abstract  class a{
    
}

class b extends a{
	public b(){
		super();
	}
}

抽象方法表示没有实现的方法,没有方法体的方法

  • 没有方法体,以分号结尾
  • 前面的修饰符列表中有abstract关键字
    比如public abstract void dosome();

但是java语言中凡是没有方法体的方法都是抽象方法(×)
因为Object类中就有很多方法都没有方法体,都是以“;”结尾的,但他们都不是抽象方法

public class AbstractTest03 {
    public static void main(String[] args) {
//此时不能再 new Employee 了
        Person p = new Employee();
    } }
abstract class Person {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    //此方法各个子类都可以使用
    public void commonMethod1() {
        System.out.println("---------commonMethod1-------");
    }
    //采用 abstract 定义抽象方法
    public abstract void printInfo();
}
abstract class Employee extends Person {
    //再次声明该方法为抽象的
    public abstract void printInfo();
}
class Student extends Person {
    //实现抽象的方法
    public void printInfo() {
        System.out.println("Student.printInfo()");
    } 
    }

1.3 接口

接口是特殊的抽象类,类与类是继承extends,类与接口是实现implements,其实都是继承

  • 接口是一种“引用数据类型”,完全抽象的,支持多继承,且一个接口可以继承多个接口,只有常量+抽象方法
  • 所有的元素都是public修饰的,抽象方法的public abstract可以省略,常量的public static final可以省略,方法不能有方法体

定义的格式

[修饰符列表] interface 接口名{}

支持多继承,且一个接口可以继承多个接口
每一个interface 都会生成一个class后缀名的文件

public class text1 {
    public static void main(String[] args) {
	}
}

interface a{
}

interface b extends a{
}

interface c extends a,b{
}

接口中的方法是抽象,所以不能有方法体
定义抽象方法的时候可以省略修饰符public abstract

interface a{
	int sum(int a,intb);//可以省略,默认系统会给你加上
	void dosome();//可以执行通过
	void dosome(){};//不可以执行通过,不能有方法体
}

常量的定义结合final 是public static final可以省路

public class text1 {
    public static void main(String[] args) {
    	//定义输出的时候 需要用类名加变量 也就是a.i
    	//而且不允许在修改常量
	}
}
interface a{
	public static final int i=100;
	int z=1;//z也是一个常量而不是变量 不能修改
}

1.4 接口与抽象类结合

当一个非抽象的类实现接口的话,必须将接口中所有的抽象方法全部实现(覆盖、重写)

// 特殊的抽象类,完全抽象的,叫做接口。
interface MyMath{
	double PI = 3.1415926;
	int sum(int a, int b);
	int sub(int a, int b);
}

// 这样没问题
abstract class MyMathImpl implements MyMath {
}

//编译出错
class MyMathImpl implements MyMath {
}

//添加方法重写实现
//修正
class MyMathImpl implements MyMath {

	//错误:正在尝试分配更低的访问权限; 以前为public
	/*
	int sum(int a, int b){
		return a + b;
	}
	*/

	// 重写/覆盖/实现 接口中的方法(通常叫做实现。)
	public int sum(int a, int b){
		return a + b;
	}

	public int sub(int a, int b){
		return a - b;
	}
}

接口中的方法必须是public :
如果删除执行不通过,因为接口是public,如果继承后,不写public执行后是分配更低的权限这是不行的,所以要添加public

结合多态面向接口编程

public class Test02{
	public static void main(String[] args){
		//错误: MyMath是抽象的; 无法实例化
		//new MyMath();

		// 父类型的引用指向子类型的对象
		MyMath mm = new MyMathImpl();
		// 调用接口里面的方法(面向接口编程。)
		int result1 = mm.sum(10, 20);
		System.out.println(result1);

		int result2 = mm.sub(20, 10);
		System.out.println(result2);
	}
}

接口与接口之间可以多继承,一个类也可以多个接口

interface X{
}
interface Y{
}
interface Z extends X,Y{ //接口和接口支持多继承。
}

一个类可以实现多个接口
java中类和类只支持单继承。实际上单继承是为了简单而出现的,java中的接口弥补了单继承带来的缺陷

interface A{
	void m1();
}

interface B{
	void m2();
}

interface C{
	void m3();
}

// 实现多个接口,其实就类似于多继承。
class D implements A,B,C{
	// 实现A接口的m1()
	public void m1(){
		
	}
	// 实现B接口中的m2()
	public void m2(){
		System.out.println("m2 ....");
	}
	// 实现接口C中的m3()
	public void m3(){
	
	}
}

强转类型的时候,需要有继承关系才可

A a = new D();
//a.m2(); // 编译报错。A接口中没有m2()方法。
B b = new D();
C c = new D();

// 这个编译没问题,运行也没问题。
// 调用其他接口中的方法,你需要转型(接口转型。)
B b2 = (B)a;
b2.m2();

// 直接向下转型为D可以吗?可以
D d = (D)a;
d.m2();

但如果没有继承关系不能强转

M m = new E();

// 经过测试:接口和接口之间在进行强制类型转换的时候,没有继承关系,也可以强转。
// 但是一定要注意,运行时可能会出现ClassCastException异常。
// 编译没问题,运行有问题。
K k = (K)m;

if(m instanceof K){
K k = (K)m;

interface K{
}

interface M{
}

class E implements M{
}

继承和实现同时存在
extends在前 implements在后

public class Test04{
	public static void main(String[] args){
		// 创建对象(表面看Animal类没起作用!)
		Flyable f = new Cat(); //多态。
		f.fly();
	}
}

// 动物类:父类
class Animal{
}


interface Flyable{
	void fly();
}

// 动物类子类:猫类
// Flyable是一个接口,是一对翅膀的接口,通过接口插到猫身上,让猫变的可以飞翔。
class Cat extends Animal implements Flyable{
	public void fly(){
		System.out.println("飞猫起飞,翱翔太空的一只猫,很神奇,我想做一只猫!!");
	}
}

抽象类和接口的区别:

  • 抽象类是半抽象的,有构造方法,只允许出现常量和抽象方法。类和类之间只能单继承,一个抽象类只能继承一个类(单继承)。

  • 接口是完全抽象的,没有构造方法,接口和接口之间支持多继承,一个类可以同时实现多个接口

1.5 接口的作用

接口在开发中的作用,类似于多态在开发中的作用

  • 多态:面向抽象编程,不要面向具体编程。降低程序的耦合度。提高程序的扩展力
class Fish implements Flyable{ //没写extends,也是有的,默认继承Object。
	public void fly(){
		System.out.println("我是六眼飞鱼(流言蜚语)!!!");
	}

/*
public class Master{
	public void feed(Dog d){}
	public void feed(Cat c){}
	//假设又要养其它的宠物,那么这个时候需要再加1个方法。(需要修改代码了)
	//这样扩展力太差了,违背了OCP原则(对扩展开放,对修改关闭。)
}
*/
	
public class Master{
	public void feed(Animal a){
	// 面向Animal父类编程,父类是比子类更抽象的。
	//所以我们叫做面向抽象编程,不要面向具体编程。
	//这样程序的扩展力就强。
	}
}
  • 接口的使用离不开多态机制。(接口+多态才可以达到降低耦合度。)
  • 接口可以解耦合

私有属性最好是get和set,放到外围访问不了,所以尽量调用get和set执行

类和类之间的关系有继承、关联、接口等。

1.6 包和import

psckage:

  • package出现在java源文件第一行
  • 编译:javac -d . xxx.java
  • 运行:java 完整类名
javac -d . HelloWorld.java

// javac 负责编译的命令
//-d 带包编译
//	. 代表编译之后生成的东西放到当前目录下(点代表当前目录)
//	HelloWorld.java  被编译的java文件名。

import:
import什么时候使用:

  • A类中使用B类。
  • A和B类都在同一个包下。不需要import。
  • A和B类不在同一个包下。需要使用import
  • java.lang.*;这个包下的类不需要使用import导入(system输出语句也在这个包下)

import语句只能出现在package语句之下,class声明语句之上。

1.7 访问控制权限

访问控制权限有四个:

  • private 表示私有的,只能在本类中访问
  • public 表示公开的,在任何位置都可以访问
  • “默认”表示只能在本类,以及同包下访问
  • protected表示只能在本类、同包、子类中访问
访问控制修饰符本类同包子类任意位置
private可以不可以不可以不可以
public可以可以可以可以
protected可以可以可以不可以
默认可以可以不可以不可以

访问修饰符可以修饰的属性可以修饰

  • 属性(4个都能用)
  • 方法(4个都能用)
  • 类(public和默认能用,其它不行。)
  • 接口(public和默认能用,其它不行。)

1.8 Object类

主要讲解以下常用类:
protected Object clone() // 负责对象克隆的。
int hashCode() // 获取对象哈希值的一个方法。
boolean equals(Object obj) // 判断两个对象是否相等
String toString() // 将对象转换成字符串形式
protected void finalize() // 垃圾回收器负责调用的方法

1. toString()类
可查看我之前写的一篇文章
java中Arrays.toString()详细分析(全)

本身的源代码是

public String toString() {
	return this.getClass().getName() + "@" + Integer.toHexString(hashCode());
}

输出的结果是一个类名+16进制。
所以我们要改写此方法输出
完整代码如下:
可以改写toString()后 直接调用这个即可,不用在重新赋值给字符串在输出

public class Test01{
	public static void main(String[] args){
		MyTime t1 = new MyTime(1970, 1, 1);
		// 一个日期对象转换成字符串形式的话
		String s1 = t1.toString();

		//MyTime类重写toString()方法之前
		//System.out.println(s1); // MyTime@28a418fc
		
		//MyTime类重写toString()方法之后
		System.out.println(s1); // 1970年1月1日

		
		//System.out.println(t1.toString()); //1970年1月1日

		// 注意:输出引用的时候,会自动调用该引用的toString()方法。
		System.out.println(t1);
	}
}
class MyTime{
	int year;
	int month;
	int day;

	public MyTime(){
	
	}

	public MyTime(int year, int month, int day){
		this.year = year;
		this.month = month;
		this.day = day;
	}

	// 重写toString()方法
	public String toString(){
		//return this.year + "年" + this.month + "月" + this.day + "日";
		return this.year + "/" + this.month + "/" + this.day;
	}
}

2. equals()类
通过equals方法来判断两个对象是否相等
在Object类中的equals方法当中,默认采用的是==判断两个java对象是否相等
而判断的是两个java对象的内存地址,我们应该判断两个java对象的内容是否相等。需要子类重写equals

源码如下:

equals方法的源代码
public boolean equals(Object obj) {
	return (this == obj);
}

判断两个java对象是否相等,不能使用“==”,因为比较的是两个对象的内存地址
所以需要比较内容,需要改写equals

public boolean equals(Object obj) {
	// 当年相同,月相同,并且日也相同的时候,表示两个日期相同。两个对象相等。
	// 获取第一个日期的年月日
	int year1 = this.year;
	int month1 = this.month;
	int day1 = this.day;

	// 获取第二个日期的年月日
	//int year2 = obj.year;
	//int month2 = obj.month;
	//int day2 = obj.day;

	if(obj instanceof MyTime){
		MyTime t = (MyTime)obj;
		int year2 = t.year;
		int month2 = t.month;
		int day2 = t.day;
		if(year1 == year2 && month1 == month2 && day1 == day2){
			return true;
		}
	}
	// 程序能够执行到此处表示日期不相等。
	return false;
}

但如果传进了null指针,可以运行,只不过效率会低

MyTime t4 = null;
System.out.println(t1.equals(t4)); //false

改进后的equals方法,一开始增加一些判断条件
obj是空指针,obj不是比较对象,地址是否一样(不是null,是比较对象类型)

// 改良equals方法
public boolean equals(Object obj) {
	// 如果obj是空,直接返回false
	if(obj == null){
		return false;
	}
	// 如果obj不是一个MyTime,没必要比较了 ,直接返回false
	if(!(obj instanceof MyTime)){
		return false;
	}
	// 如果this和obj保存的内存地址相同,没必要比较了,直接返回true。
	// 内存地址相同的时候指向的堆内存的对象肯定是同一个。
	if(this == obj){
		return true;
	}
	// 程序能够执行到此处说明什么?
	// 说明obj不是null,obj是MyTime类型。
	MyTime t = (MyTime)obj;
	if(this.year == t.year && this.month == t.month && this.day == t.day){
		return true;
	}

	// 程序能到这里返回false
	return false;
}

整理以上代码并合并
改进后的代码如下

public boolean equals(Object obj) {
	if(obj == null || !(obj instanceof MyTime)){
		return false;
	}
	if(this == obj){
		return true;
	}
	MyTime t = (MyTime)obj;
	return this.year == t.year && this.month == t.month && this.day == t.day ;
}

或者直接通过alt+insert生成idea的equals和toString快捷键
在这里插入图片描述


以上都是判断对象是否相等,那String类的字符串是否也可以用==号判断?

public class Test03{
	public static void main(String[] args){

		// 大部分情况下,采用这样的方式创建字符串对象
		String s1 = "hello";
		String s2 = "abc";

		// 实际上String也是一个类。不属于基本数据类型。
		// 既然String是一个类,那么一定存在构造方法。
		String s3 = new String("Test1");
		String s4 = new String("Test1");
		// new两次,两个对象内存地址,s3保存的内存地址和s4保存的内存地址不同。
		// == 判断的是内存地址。不是内容。
		System.out.println(s3 == s4); // false

		// 比较两个字符串能不能使用双等号?
		// 不能,必须调用equals方法。
		// String类已经重写equals方法了。
		System.out.println(s3.equals(s4)); // true

		// String类有没有重写toString方法呢?
		String x = new String("码农研究僧");
		// 如果String没有重写toString()方法,输出结果:java.lang.String@十六进制的地址
		// 经过测试:String类已经重写了toString()方法。
		System.out.println(x.toString()); //码农研究僧
		System.out.println(x); //码农研究僧
	}
}

故String对象比较的时候必须用equals方法

int no; //基本数据类型,比较时使用:==
	// 所在学校
String school; //引用数据类型,比较时使用:equals方法。

public boolean equals(Object obj){
	if(obj == null || !(obj instanceof Student)) return false;
	if(this == obj) return true;
	Student s = (Student)obj;
	return this.no == s.no && this.school.equals(s.school);
}

完整代码演示:

// equals方法重写的时候要彻底。

public class Test05{
	public static void main(String[] args){
		
		// 多态(自动类型转换。)
		Object o1 = new String("hello world!");
		Object o2 = new User();
		Object o3 = new Address();

		User u1 = new User("zhangsan", new Address("北京","大兴区","11111"));
		User u2 = new User("zhangsan", new Address("北京","大兴区","11111"));

		System.out.println(u1.equals(u2)); // true

		User u3 = new User("zhangsan", new Address("北京","朝阳区","11112"));
		System.out.println(u1.equals(u3)); // false
	}
}

class User{
	// 用户名
	String name; 
	// 用户的住址
	Address addr;

	public User(){
	}
	public User(String name, Address addr){
		this.name = name;
		this.addr = addr;
	}

	// 重写equals方法
	// 重写规则:当一个用户的用户名和家庭住址都相同,表示同一个用户。
	// 这个equals判断的是User对象和User对象是否相等。
	public boolean equals(Object obj){
		// 用户名和用户名相同,住址和住址相同的时候,认定是同一个用户。
		if(obj == null || !(obj instanceof User)) return false;
		if(this == obj) return true;
		
		User u = (User)obj;
		if(this.name.equals(u.name) && this.addr.equals(u.addr)){
			return true;
		}
		return false;
	}
}

class Address{
	String city;
	String street;
	String zipcode;

	public Address(){
	
	}
	public Address(String city,String street,String zipcode){
		this.city = city;
		this.street = street;
		this.zipcode = zipcode;
	}

	// 注意:这里并没有重写equals方法。
	// 这里的equals方法判断的是:Address对象和Address对象是否相等。
	public boolean equals(Object obj){
		if(obj == null || !(obj instanceof Address)) return false;
		if(this == obj) return true;
		// 怎么算是家庭住址相同呢?
		// 城市相同,街道相同,邮编相同,表示相同。
		Address a = (Address)obj;
		if(this.city.equals(a.city) 
			&& this.street.equals(a.street) 
			&& this.zipcode.equals(a.zipcode)){
			return true;
		}
		return false;
	}
}

3. finalize方法
jdk13后已经过时了

  • 这个方法是protected修饰的
  • 源代码是protected void finalize() throws Throwable { }
  • 手动调用,JVM的垃圾回收器负责调用这个方法
  • finalize()只需要重写,重写完将来自动会有程序来调用
  • 垃圾回收器不是轻易启动,可能垃圾太少或者时间不对
public class Test06{
	public static void main(String[] args){
	
		// 创建对象
		Person p = new Person();

		p = null;
	}
}

class Person{

	// 重写finalize()方法
	// Person类型的对象被垃圾回收器回收的时候,垃圾回收器负责调用:p.finalize();
	protected void finalize() throws Throwable {
		// this代表当前对象
		System.out.println(this + "即将被销毁!");
	}

}

4. finalize方法

hashCode()方法返回的是哈希码,实际上就是一个java对象的内存地址,经过哈希算法,得出的一个值public native int hashCode();

public class Test07{
	public static void main(String[] args){
		Object o = new Object();
		int hashCodeValue = o.hashCode();

		// 对象内存地址经过哈希算法转换的一个数字。可以等同看做内存地址。
		System.out.println(hashCodeValue); //798154996
	}
}

1.9 匿名内部类

  • 内部类:在类的内部又定义了一个新的类。被称为内部类
  • 可读性差,复杂,乱
  • 内部类的分类:
    静态内部类:类似于静态变量
    实例内部类:类似于实例变量
    局部内部类:类似于局部变量
class Test01{
	// 该类在类的内部,所以称为内部类
	// 由于前面有static,所以称为“静态内部类”
	static class Inner1{
	}
}
  • 匿名:没有名字的类
// main方法,入口
public static void main(String[] args){
	// 调用MyMath中的mySum方法。
	MyMath mm = new MyMath();
	/*
	Compute c = new ComputeImpl();
	mm.mySum(c, 100, 200);
	*/
	
	//合并(这样写代码,表示这个类名是有的。类名是:ComputeImpl)
	//mm.mySum(new ComputeImpl(), 100, 200);
	
	mm.mySum(new Compute(){
		public int sum(int a, int b){
			return a + b;
		}, 200, 300);



}

}

// 负责计算的接口
interface Compute{ 
	
	// 抽象方法
	int sum(int a, int b);
}

// 你自动会在这里编写一个Compute接口的实现类

class ComputeImpl implements Compute{

	// 对方法的实现
	public int sum(int a, int b){
		return a + b;
	}
}


// 数学类
class MyMath{
	// 数学求和方法
	public void mySum(Compute c, int x, int y){
		int retValue = c.sum(x, y);
		System.out.println(x + "+" + y + "=" + retValue);
	}	
}


2. 数组

可结合我之前写的一篇文章,比较全,这一数组章节为综合阐述梗概
java数组的定义与使用

  • 数组是一种引用数据类型,数组对象是堆内存当中
  • 数组元素的类型可以是基本类型,也可以是引用类型,但同一个数组只能是同一种类型,数组中存储的元素类型统一
  • 数组的长度在数组对象创建后就确定无法修改
  • 外数组还包括一个成员属性 length表示长度
  • 数组中的元素内存地址(存储的每一个元素都是有规则的挨着排列的)是连续的。内存地址连续

优点
查询/查找/检索某个下标上的元素时效率极高。
第一:每一个元素的内存地址在空间存储上是连续的。
第二:每一个元素类型相同,所以占用空间大小一样。
第三:知道第一个元素内存地址,知道每一个元素占用空间的大小,又知道下标,所以通过一个数学表达式就可以计算出某个下标上元素的内存地址。直接通过内存地址定位元素,所以数组的检索效率是最高的。
缺点:
第一:由于为了保证数组中每个元素的内存地址连续,所以在数组上随机删除或者增加元素的时候,效率较低,因为随机增删元素会涉及到后面元素统一向前或者向后位移的操作。
第二:很难在内存空间上找到一块特别大的连续的内存空间。

2.1 一维数组的声明和使用

数组声明的具体格式

  • 数组元素的类型[] 变量名称(比较常用这一种)
  • 数组元素的类型 变量名称[]
int [] a;
Student[] stu;

//在一行中也可以声明多个数组
int[] a, b, c;

基本常量数组创建的具体格式

  • 使用 new 操作符来创建数组,new 数组元素的数据类型[数组元素的个数]
  • 楚数组为引用类型,它在堆中分配

数组下标不可越界,可用遍历数组进行输出以及赋值

int[] data = new int[5];

for (int i=0; i<data.length; i++) {
System.out.println(data[i]);
}

引用类型数组创建的具体格式
采用引用类型的数组,

public class ArrayTest02 {
    public static void main(String[] args) {
//声明引用类型的数组
        Student[] student = new Student[2];
//出现空指针
//因为引用类型的数组,它采用 null 作为默认的初始化值
        student[0].id = 1001;
        student[0].name = "张三";
        student[1].id = 1002;
        student[1].name = "李四";
    } }
class Student {
    int id;
    String name;
}    
   

静态数组与动态数组如何选择定义

  • 确定数组中存储哪些具体的元素时,采用静态初始化方式。
  • 不确定将来数组中存储哪些数据,你可以采用动态初始化的方式,预先分配内存空间。
//静态初始化语法格式:
    int[] array = {100, 2100, 300, 55};
//动态初始化语法格式:
    int[] array = new int[5]; // 这里的5表示数组的元素个数。
                                // 初始化一个5个长度的int类型数组,每个元素默认值0
    String[] names = new String[6]; // 初始化6个长度的String类型数组,每个元素默认值null。

匿名数组直接输出即可new int[3] ,默认是系统初始化值

// 如果直接传递一个静态数组的话,语法必须这样写。
printArray(new int[]{1,2,3});

静态初始化数组默认是0而不是null,留给用户操作初始化
结合多态以及接口的使用,在静态数组和动态数组的调用

public class  ArrayTest07 {
    public static void main(String[] args) {

        // a是一个数组
        // a[0] 是数组中的一个元素。
        // a[1] 是数组中的一个元素。
        int[] a = {100, 200, 300};
        System.out.println(a[1]);
        //a[2] = 400;

        int[] array = {1,2,3};
        for (int i = 0; i < array.length; i++) {
            /*int temp = array[i];
            System.out.println(temp);*/
            System.out.println(array[i]);
        }

        // 创建一个Animal类型的数组
        Animal a1 = new Animal();
        Animal a2 = new Animal();
        Animal[] animals = {a1, a2};

        // 对Animal数组进行遍历
        for (int i = 0; i < animals.length; i++) {
            /*Animal a = animals[i];
            a.move();*/
            // 代码合并
            animals[i].move(); // 这个move()方法不是数组的。是数组当中Animal对象的move()方法。
        }

        // 动态初始化一个长度为2的Animal类型数组。
        Animal[] ans = new Animal[2];
        // 创建一个Animal对象,放到数组的第一个盒子中。
        ans[0] = new Animal();

        // Animal数组中只能存放Animal类型,不能存放Product类型。
        //ans[1] = new Product();

        // Animal数组中可以存放Cat类型的数据,因为Cat是一个Animal。
        // Cat是Animal的子类。
        ans[1] = new Cat();

        // 创建一个Animal类型的数组,数组当中存储Cat和Bird
        Cat c = new Cat();
        Bird b = new Bird();
        Animal[] anis = {c, b};

        //Animal[] anis = {new Cat(), new Bird()}; // 该数组中存储了两个对象的内存地址。
        for (int i = 0; i < anis.length; i++){
            // 这个取出来的可能是Cat,也可能是Bird,不过肯定是一个Animal
            // 如果调用的方法是父类中存在的方法不需要向下转型。直接使用父类型引用调用即可。
            //anis[i]
            //Animal an = anis[i];
            //an.move();

            //Animal中没有sing()方法。
            //anis[i].sing();

            // 调用子对象特有方法的话,需要向下转型!!!
            if(anis[i] instanceof Cat){
                Cat cat = (Cat)anis[i];
                cat.catchMouse();
            }else if(anis[i] instanceof Bird){
                Bird bird = (Bird)anis[i];
                bird.sing();
            }
        }

    }
}

class Animal{
    public void move(){
        System.out.println("Animal move...");
    }
}

// 商品类
class Product{

}

// Cat是子类
class Cat extends Animal {
    public void move(){
        System.out.println("猫在走猫步!");
    }
    // 特有方法
    public void catchMouse(){
        System.out.println("猫抓老鼠!");
    }
}

// Bird子类
class Bird extends Animal {
    public void move(){
        System.out.println("Bird Fly!!!");
    }
    // 特有的方法
    public void sing(){
        System.out.println("鸟儿在歌唱!!!");
    }
}

数组扩容拷贝
扩容是先新建一个大容量的数组,然后将小容量数组中的数据一个一个拷贝到大数组当中,数组扩容效率较低。因为涉及到拷贝的问题。所以在以后的开发中请注意:尽可能少的进行数组的拷贝
1.拷贝基本类型变量

// 拷贝源(从这个数组中拷贝)
int[] src = {1, 11, 22, 3, 4};

// 拷贝目标(拷贝到这个目标数组上)
int[] dest = new int[20]; // 动态初始化一个长度为20的数组,每一个元素默认值0

System.arraycopy(src, 0, dest, 0, src.length);
for (int i = 0; i < dest.length; i++) {
    System.out.println(dest[i]);
}

输出的结果为src的结果并且之后的空位置是系统默认赋值0
2.拷贝引用类型变量
如果是引用类型数组同样也可以拷贝,系统默认赋值为null

// 数组中如果存储的元素是引用,可以拷贝吗?当然可以。
String[] strs = {"hello", "world!", "study", "java", "oracle", "mysql", "jdbc"};
String[] newStrs = new String[20];
System.arraycopy(strs, 0, newStrs, 0, strs.length);
for (int i = 0; i < newStrs.length; i++) {
    System.out.println(newStrs[i]);
}

数组是objects同样也可以拷贝
具体拷贝的是一个数组的地址

Object[] objs = {new Object(), new Object(), new Object()};
Object[] newObjs = new Object[5];
// 思考一下:这里拷贝的时候是拷贝对象,还是拷贝对象的地址。(地址。)
System.arraycopy(objs, 0, newObjs, 0, objs.length);
for (int i = 0; i < newObjs.length; i++) {
    System.out.println(newObjs[i]);
}

截图如下
在这里插入图片描述

2.2 二维数组的声明和使用

  • 二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个一维数组
  • 维数组是一个特殊的二维数组,特殊在这个二维数组中每一个元素是一个一维数组
  • 以此类推同理
int[][] a = {
        {100, 200, 300},
        {30, 20, 40, 50, 60},
        {6, 7, 9, 1},
        {0}
};
System.out.println(a.length); // 4
System.out.println(a[0].length); // 3
System.out.println(a[1].length); // 5
System.out.println(a[2].length); // 4
System.out.println(a[3].length); // 1

二维数组的遍历
伪代码如下

// 遍历二维数组
for(int i = 0; i < array.length; i++){ // 外层循环3次。(负责纵向。)
    String[] 一维数组 = array[i];
    
    for(int j = 0; j < 一维数组.length; j++){
        System.out.print(一维数组[j] + " ");
    }
   
    System.out.println();
}

结合完整代码实例的演示

public class ArrayTest11 {
    public static void main(String[] args) {

        // 二维数组
        String[][] array = {
                {"java", "oracle", "c++", "python", "c#"},
                {"张三", "李四", "王五"},
                {"lucy", "jack", "rose"}
        };


        for(int i = 0; i < array.length; i++){ // 外层循环3次。(负责纵向。)
            for(int j = 0; j < array[i].length; j++){// 负责遍历一维数组
                System.out.print(array[i][j] + " ");
            }
            System.out.println(); // 输出换行符
        }
    }
}

动态初始化二维数组

// 3行4列。
// 3个一维数组,每一个一维数组当中4个元素。
int[][] array = new int[3][4];

结合静态初始化二维数组进行比较

// 静态初始化
int[][] a = {{1,2,3,4},{4,5,6,76},{1,23,4}};
// 没有这种语法
//printArray({{1,2,3,4},{4,5,6,76},{1,23,4}});

// 可以这样写。
printArray(new int[][]{{1,2,3,4},{4,5,6,76},{1,23,4}});

public static void printArray(int[][] array){
    // 遍历二维数组。
    for (int i = 0; i < array.length; i++) {
        for (int j = 0; j < array[i].length; j++) {
            System.out.print(array[i][j] + " ");
        }
        System.out.println();
    }
    }

2.3 排序查找算法

1.冒泡排序算法
每一次循环结束之后,都要找出最大的数据,放到参与比较的这堆数据的最右边。(冒出最大的那个气泡。),左边的数字和右边的数字比对,当左边 > 右边的时候,交换位置。

public class BubbleSort {
    public static void main(String[] args) {

        // 这是int类型的数组对象
        //int[] arr = {3, 2, 7, 6, 8};
        int[] arr = {9, 8, 10, 7, 6, 0, 11};
		int count = 0;
        for(int i = arr.length-1; i > 0; i--){
            for(int j = 0; j < i; j++){
                // 不管是否需要交换位置,总之是要比较一次的。
                //count++;
                if(arr[j] > arr[j+1]){
                    // 交换位置。
                    // arr[j] 和 arr[j+1] 交换
                    int temp;
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    count++;
                }
            }
        }

        //System.out.println("比较次数:" + count);
        System.out.println("交换位置的次数:" + count); //13
        // 输出结果
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

2.选择排序算法
每一次从这堆“参与比较的数据当中”找出最小值,这个最小值和“参与比较的这堆最前面的元素”交换位置。选择排序比冒泡排序好在:每一次的交换位置都是有意义的。

public class SelectSort {
    public static void main(String[] args) {

        //int[] arr = {3, 1, 6, 2, 5};
        int[] arr = {9, 8, 10, 7, 6, 0, 11};

        int count = 0;
        int count2 = 0;


        for(int i = 0; i < arr.length - 1; i++){
            int min = i;
            for(int j = i+1; j < arr.length; j++){
                count++;
                if(arr[j] < arr[min]){
                    min = j; //最小值的元素下标是j
                }
            }

            // 当i和min相等时,表示最初猜测是对的。
            // 当i和min不相等时,表示最初猜测是错的,有比这个元素更小的元素,
            // 需要拿着这个更小的元素和最左边的元素交换位置。
            if(min != i){
                // 表示存在更小的数据
                // arr[min] 最小的数据
                // arr[i] 最前面的数据
                int temp;
                temp = arr[min];
                arr[min] = arr[i];
                arr[i] = temp;
                count2++;
            }
        }

        // 冒泡排序和选择排序实际上比较的次数没变。
        // 交换位置的次数减少了。
        System.out.println("比较次数" + count); // 21
        System.out.println("交换次数:" + count2); // 5

        // 排序之后遍历
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

3.二分查找算法
二分法查找算法是基于排序的基础之上。(没有排序的数据是无法查找的。)
可具体查看我上一篇文章
【leetcode】二分查找

public class ArrayUtil {
    public static void main(String[] args) {

        int[] arr = {100,200,230,235,600,1000,2000,9999};

        // 找出arr这个数组中200所在的下标。
        // 调用方法
        int index = binarySearch(arr, 230);
        System.out.println(index == -1 ? "该元素不存在!" : "该元素下标" + index);
    }

    /**
     * 从数组中查找目标元素的下标。
     * @param arr 被查找的数组(这个必须是已经排序的。)
     * @param dest 目标元素
     * @return -1表示该元素不存在,其它表示返回该元素的下标。
     */
    public static int binarySearch(int[] arr, int dest) {
        // 开始下标
        int begin = 0;
        // 结束下标
        int end = arr.length - 1;
        // 开始元素的下标只要在结束元素下标的左边,就有机会继续循环。
        while(begin <= end) {
            // 中间元素下标
            int mid = (begin + end) / 2;
            if (arr[mid] == dest) {
                return mid;
            } else if (arr[mid] < dest) {
                // 目标在“中间”的右边
                // 开始元素下标需要发生变化(开始元素的下标需要重新赋值)
                begin = mid + 1; // 一直增
            } else {
                // arr[mid] > dest
                // 目标在“中间”的左边
                // 修改结束元素的下标
                end = mid - 1; // 一直减
            }
        }
        return -1;
    }

}

3. 常用类

本章节的类中阐述String、StringBuffer、基础类型对应的 8 个包装类、日期相关类、数字相关类、 Random、 Enum

3.1 String类

  • String表示字符串类型,属于引用数据类型,不属于基本数据类型
  • 在java中随便使用双引号括起来的都是String对象。如:“abc”,“def”,“hello world!”,这是3个String对象
  • 双引号括起来的字符串,是不可变的
  • 双引号括起来的字符串都是直接存储在“方法区”的“字符串常量池”当中的
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
String s3 = new String("xy");

// i变量中保存的是100这个值。
int i = 100;
// s变量中保存的是字符串对象的内存地址。
// s引用中保存的不是"abc",是0x1111
// 而0x1111是"abc"字符串对象在“字符串常量池”当中的内存地址。
String s = "abc";

内存地址如图所示
在这里插入图片描述
强化new对象调用的时候地址显示

User user = new User(110, "张三");

在这里插入图片描述
String类的字符串在双引号和新建对象的地址判断

public class StringTest02 {
    public static void main(String[] args) {
        String s1 = "hello";
        // "hello"是存储在方法区的字符串常量池当中
        // 所以这个"hello"不会新建。(因为这个对象已经存在了!)
        String s2 = "hello";
        // 分析结果是true还是false?
        // == 双等号比较的是不是变量中保存的内存地址?是的。
        System.out.println(s1 == s2); // true

        String x = new String("xyz");
        String y = new String("xyz");
        // 分析结果是true还是false?
        // == 双等号比较的是不是变量中保存的内存地址?是的。
        System.out.println(x == y); //false

        // 通过这个案例的学习,我们知道了,字符串对象之间的比较不能使用“==”
        // "=="不保险。应该调用String类的equals方法。
        // String类已经重写了equals方法,以下的equals方法调用的是String重写之后的equals方法。
        System.out.println(x.equals(y)); // true

        String k = new String("testString");
        //String k = null;
        // "testString"这个字符串可以后面加"."呢?
        // 因为"testString"是一个String字符串对象。只要是对象都能调用方法。
        System.out.println("testString".equals(k)); // 建议使用这种方式,因为这个可以避免空指针异常。
        System.out.println(k.equals("testString")); // 存在空指针异常的风险。不建议这样写。
    }
}

System.out.println(x);
System.out.println(y);
以上两个变量输出的字符串是值,是因为改写了toString类
但是如果比较对象即两个变量,比较的是地址值而不是值
在这里插入图片描述
通过比较可以得出字符串对象之间的比较不能使用“==”


补充:面试题
一共新建了多少个对象?

/*
String s1 = new String("hello");
String s2 = new String("hello");

方法区字符串常量池中有1个:“hello”,堆内存当中有两个String对象。一共3个


关于String类中的构造方法:

  • String s = new String("");
  • 第二个:String s = “”; 最常用
  • 第三个:String s = new String(char数组);
  • 第四个:String s = new String(char数组,起始下标,长度);
  • 第五个:String s = new String(byte数组);
  • 第六个:String s = new String(byte数组,起始下标,长度)
String s1 =  "hello world!";
String s2 = new String(bytes);
String s3 = new String(bytes, 1, 2);
char[] chars = {'我','是','中','国','人'};
String s5 = new String(chars, 2, 3);	
String s6 = new String("helloworld!");

额外补充

// s1这个变量中保存的是一个内存地址。
// 按说以下应该输出一个地址。
// 但是输出一个字符串,说明String类已经重写了toString()方法。
System.out.println(s1);//hello world!
System.out.println(s1.toString()); //hello world!

// 前面说过:输出一个引用的时候,会自动调用toString()方法,默认Object的话,会自动输出对象的内存地址。
// 通过输出结果我们得出一个结论:String类已经重写了toString()方法。
// 输出字符串对象的话,输出的不是对象的内存地址,而是字符串本身。
System.out.println(s2.toString()); //abc
System.out.println(s2); //abc

3.11 charAt方法

具体用法可看我上一篇文章
java中charAt用法详细分析(全)
返回指定索引值

//1(掌握).char charAt(int index)
char c = "中国人".charAt(1); // "中国人"是一个字符串String对象。只要是对象就能“点.”
System.out.println(c); // 国

3.12 compare To方法

返回的是一个int值,按照字典顺序的比较值
返回值等于,代表相等
返回值小于0,前小后大
返回值大于0,前大后小
每一个字母一个字母比较,如果有结果了之后,后面的内容就不比较了,类似英语的字典。

// 字符串之间比较大小不能直接使用 > < ,需要使用compareTo方法。
int result = "abc".compareTo("abc");
System.out.println(result); //0(等于0) 前后一致  10 - 10 = 0

int result2 = "abcd".compareTo("abce");
System.out.println(result2); //-1(小于0) 前小后大 8 - 9 = -1

int result3 = "abce".compareTo("abcd");
System.out.println(result3); // 1(大于0) 前大后小 9 - 8 = 1

// 拿着字符串第一个字母和后面字符串的第一个字母比较。能分胜负就不再比较了。
System.out.println("xyz".compareTo("yxz")); // -1

3.13 contains方法

判断前面的字符串中是否有包含后面的字符串.boolean contains(CharSequence s)

 // 判断前面的字符串中是否包含后面的子字符串。
System.out.println("HelloWorld.java".contains(".java")); // true
    System.out.println("http://www.baidu.com".contains("https://")); // false

3.14 endsWith方法

判断当前字符串是否以某个子字符串结尾. boolean endsWith(String suffix)

System.out.println("test.txt".endsWith(".java")); // false
System.out.println("test.txt".endsWith(".txt")); // true
System.out.println("fdsajklfhdkjlsahfjkdsahjklfdss".endsWith("ss")); // true

3.15 equals方法

比较两个字符串必须使用equals方法,不能使用“==”,.boolean equals(Object anObject)

// equals只能看出相等不相等。
// compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。
System.out.println("abc".equals("abc")); // true

3.16 equalsIgnoreCase方法

判断两个字符串是否相等,并且同时忽略大小写。.boolean equalsIgnoreCase(String anotherString)

System.out.println("ABc".equalsIgnoreCase("abC")); // true

3.17 getBytes方法

将字符串对象转换成字节数组.byte[] getBytes()

byte[] bytes = "abcdef".getBytes();
for(int i = 0; i < bytes.length; i++){
    System.out.println(bytes[i]); //97 98 99 100 101 102
}

3.18 indexOf与lastIndexOf方法

判断某个子字符串在当前字符串中第一次出现处的索引(下标).int indexOf(String str)

System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java")); // 6

判断某个子字符串在当前字符串中最后一次出现的索引(下标).int lastIndexOf(String str)

System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java")); //22

3.19 isEmpty()方法

判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。.boolean isEmpty()
如果为null会出现空指针异常

//String s = ""; //true
String s = "a"; //false
System.out.println(s.isEmpty());

补充判断数组长度是length属性,判断字符串长度是length()方法。

// 10(掌握). int length()
// 面试题:判断数组长度和判断字符串长度不一样

System.out.println("abc".length()); // 3

System.out.println("".length()); // 0

3.20 replace方法

替换某一部分. String replace(CharSequence target, CharSequence replacement)
String的父接口就是:CharSequence


String newString = "http://www.baidu.com".replace("http://", "https://");
System.out.println(newString); //https://www.baidu.com
// 把以下字符串中的“=”替换成“:”
String newString2 = "name=zhangsan&password=123&age=20".replace("=", ":");
System.out.println(newString2); //name:zhangsan&password:123&age:20

3.21 split方法

拆分字符串.String[] split(String regex)

String[] ymd = "1980-10-11".split("-"); //"1980-10-11"以"-"分隔符进行拆分。
for(int i = 0; i < ymd.length; i++){
    System.out.println(ymd[i]);
}
String param = "name=zhangsan&password=123&age=20";
String[] params = param.split("&");
for(int i = 0; i <params.length; i++){
    System.out.println(params[i]);
    // 可以继续向下拆分,可以通过“=”拆分。
}

3.22 startsWith方法

判断某个字符串是否以某个子字符串开始。、boolean startsWith(String prefix)

System.out.println("http://www.baidu.com".startsWith("http")); // true
System.out.println("http://www.baidu.com".startsWith("https")); // false

3.23 substring方法

截取字符串 .String substring(int beginIndex) 参数是起始下标

System.out.println("http://www.baidu.com".substring(7)); //www.baidu.com

多一个起始位置和终止位置.String substring(int beginIndex, int endIndex)

// beginIndex起始位置(包括)
// endIndex结束位置(不包括)
System.out.println("http://www.baidu.com".substring(7, 10)); //www

3.24 toCharArray方法

具体可看我上一篇文章
java中toCharArray用法详细分析(全)
将字符串转换成char数组.char[] toCharArray()

char[] chars = "我是中国人".toCharArray();
for(int i = 0; i < chars.length; i++){
    System.out.println(chars[i]);
}

3.25 toLowerCase与toUpperCase方法

大小写字母的转换如下


// 转换为小写。
System.out.println("ABCDefKXyz".toLowerCase());
//转换为大写
System.out.println("ABCDefKXyz".toUpperCase());

3.26 trim方法

去除字符串前后空白.String trim();

System.out.println("           hello      world             ".trim());

3.27 valueOf方法

将“非字符串”转换成“字符串”
String中只有一个方法是静态的,不需要new对象

//String s1 = String.valueOf(true);
//String s1 = String.valueOf(100);
//String s1 = String.valueOf(3.14);

如果没有重写对象的toString方法,输出对象@地址

String s1 = String.valueOf(new Customer());
//System.out.println(s1); // 没有重写toString()方法之前是对象内存地址 对象@地址

System.out.println(s1); //码农研究僧!!!
class Customer {
    // 重写toString()方法

    @Override
    public String toString() {
        return "码农研究僧!!!!";
    }
}

3.2 stringbuffer类

java中的字符串是不可变的,由于频繁造成字符串的拼接,每一次拼接都会产生新字符串,这样会占用大量的方法区内存。造成内存空间的浪费String s = "abc"; s += "hello";就单纯这两行代码就会产生三个代码,abc,abchello,hello
大量的拼接可以使用两个类

  • java.lang.StringBuffer
  • java.lang.StringBuilder

StringBuffer()无参构造,构造一个其中不带字符的缓冲区,初始化容量为16个字符
StringBuffer(int capacity)构造一个不带字符,但具有指定初始化容量的字符串缓冲区

public class StringBufferTest02 {
    public static void main(String[] args) {

        // 创建一个初始化容量为16个byte[] 数组。(字符串缓冲区对象)
        StringBuffer stringBuffer = new StringBuffer();

        // 拼接字符串,以后拼接字符串统一调用 append()方法。
        // append是追加的意思。
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("d");
        stringBuffer.append(3.14);
        stringBuffer.append(true);
        // append方法底层在进行追加的时候,如果byte数组满了,会自动扩容。
        stringBuffer.append(100L);

        System.out.println(stringBuffer.toString());

        // 指定初始化容量的StringBuffer对象(字符串缓冲区对象)
        StringBuffer sb = new StringBuffer(100);
        sb.append("hello");
        sb.append("world");
        sb.append("hello");
        sb.append("kitty");

        System.out.println(sb);
    }
}

StringBuffer和StringBuilder的区别?

  • StringBuffer中的方法都有:synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。
  • StringBuilder中的方法都没有:synchronized关键字修饰,表StringBuilder在多线程环境下运行是不安全的。
public class StringBuilderTest01 {
    public static void main(String[] args) {

        // 使用StringBuilder也是可以完成字符串的拼接。
        StringBuilder sb = new StringBuilder();
        sb.append(100);
        sb.append(true);
        sb.append("hello");
        sb.append("kitty");
        System.out.println(sb);
    }
}

补充:String为什么是不可变的?StringBuilder/StringBuffer为什么是可变的呢?

源代码String类中有一个byte[]数组,这个byte[]数组采用了final修饰,因为数组一旦创建长度不可变。并且被final修饰的引用一旦指向某个对象之后,不可再指向其它对象,所以String是不可变的!“abc” 无法变成 “abcd”

源代码StringBuffer/StringBuilder内部实际上是一个byte[]数组, 这个byte[]数组没有被final修饰,StringBuffer/StringBuilder的初始化容量我记得应该是16,当存满之后会进行扩容,底层调用了数组拷贝的方System.arraycopy()…是这样扩容的。所以StringBuilder/StringBuffer适合于使用字符串的频繁拼接操作。

public class StringBufferTest04 {
    public static void main(String[] args) {

        // 字符串不可变是什么意思?
        // 是说双引号里面的字符串对象一旦创建不可变。
        String s = "abc"; //"abc"放到了字符串常量池当中。"abc"不可变。

        // s变量是可以指向其它对象的。
        // 字符串不可变不是说以上变量s不可变。说的是"abc"这个对象不可变。
        s = "xyz";//"xyz"放到了字符串常量池当中。"xyz"不可变。

    }
}

3.3 基础类型对应的 8 个包装类

入门小程序:

数字属于基本数据类型,而doSome()方法参数的类型是Object,可以传一个数字对应的包装类进去

public class IntegerTest01 {

    //入口
    public static void main(String[] args) {

        // 把100这个数字经过构造方法包装成对象。
        MyInt myInt = new MyInt(100);
        // doSome()方法虽然不能直接传100,但是可以传一个100对应的包装类型。
        doSome(myInt);
    }

    public static void doSome(Object obj){
        //System.out.println(obj);
        System.out.println(obj.toString());
    }
}

// 这种包装类目前是我自己写的。实际开发中我们不需要自己写。
// 8种基本数据类型对应的8种包装类,SUN公司已经写好了。我们直接用。
public class MyInt {
    int value;

    public MyInt() {
    }

    public MyInt(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

8种基本数据类型不够用,而8种基本数据类型对应的包装类型名是

基本数据类型包装类型
bytejava.lang.Byte(父类Number)
shortjava.lang.Short(父类Number)
intjava.lang.Integer(父类Number)
longjava.lang.Long(父类Number)
floatjava.lang.Float(父类Number)
doublejava.lang.Double(父类Number)
booleanjava.lang.Boolean(父类Object)
charjava.lang.Character(父类Object)

只有 java.lang.Integer(父类Number)和java.lang.Character(父类Object)这两个与基本类型不一样有所区别。
装箱拆箱的操作。都是包装好的函数
Number类中有这样的方法

  • byte byteValue() 以 byte 形式返回指定的数值。
  • abstract double doubleValue()以 double 形式返回指定的数值。
  • abstract float floatValue()以 float 形式返回指定的数值。
  • abstract int intValue()以 int 形式返回指定的数值。
  • abstract long longValue()以 long 形式返回指定的数值。
  • short shortValue()以 short 形式返回指定的数值。

这些方法其实所有的数字包装类的子类都有,这些方法是负责拆箱的。

public class IntegerTest02 {
    public static void main(String[] args) {

        // 123这个基本数据类型,进行构造方法的包装达到了:基本数据类型向引用数据类型的转换。
        // 基本数据类型 -(转换为)->引用数据类型(装箱)
        Integer i = new Integer(123);

        // 将引用数据类型--(转换为)-> 基本数据类型
        float f = i.floatValue();
        System.out.println(f); //123.0

        // 将引用数据类型--(转换为)-> 基本数据类型(拆箱)
        int retValue = i.intValue();
        System.out.println(retValue); //123
    }
}

3.31 自动装箱和自动拆箱

  • 自动装箱:基本数据类型自动转换成包装类。
  • 自动拆箱:包装类自动转换成基本数据类型。

有了自动拆箱之后,Number类中的方法就用不着了!而且更容易编程
只有+ - * /等运算的时候才会触发装箱和拆箱的操作,详情可看下面的代码

public class IntegerTest05 {
    public static void main(String[] args) {

        // 900是基本数据类型
        // x是包装类型
        // 基本数据类型 --(自动转换)--> 包装类型:自动装箱
        Integer x = 900;
        System.out.println(x);

        // x是包装类型
        // y是基本数据类型
        // 包装类型 --(自动转换)--> 基本数据类型:自动拆箱
        int y = x;
        System.out.println(y);

        // z是一个引用,z是一个变量,z还是保存了一个对象的内存地址。
        Integer z = 1000; // 等同于:Integer z = new Integer(1000);
        // 分析为什么这个没有报错呢?
        // +两边要求是基本数据类型的数字,z是包装类,不属于基本数据类型,这里会进行自动拆箱。将z转换成基本数据类型
        // 在java5之前你这样写肯定编译器报错。
        System.out.println(z + 1);

        Integer a = 1000; // Integer a = new Integer(1000); a是个引用,保存内存地址指向对象。
        Integer b = 1000; // Integer b = new Integer(1000); b是个引用,保存内存地址指向对象。
        // == 比较的是对象的内存地址,a和b两个引用中保存的对象内存地址不同。
        // == 这个运算符不会触发自动拆箱机制。
        System.out.println(a == b); //false
    }
}

java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,放到了一个方法区的“整数型常量池”当中了,目的是只要用这个区间的数据不需要再new了,直接从整数型常量池当中取出来。

原理:x变量中保存的对象的内存地址和y变量中保存的对象的内存地址是一样的

Integer a = 128;
Integer b = 128;
System.out.println(a == b); //false

Integer x = 127;
Integer y = 127;
// == 永远判断的都是两个对象的内存地址是否相同。
System.out.println(x == y); //true
// 手动装箱
Integer x = new Integer(1000);

// 手动拆箱。
int y = x.intValue();
System.out.println(y);

Integer a = new Integer("中文");//出现异常

补充异常的类型:

  • 空指针异常:NullPointerException
  • 类型转换异常:ClassCastException
  • 数组下标越界异常:ArrayIndexOutOfBoundsException
  • 数字格式化异常:NumberFormatException

3.32 Integer常用方法

Integer类的构造方法,有两个:

  • Integer(int)
  • Integer(String)
// Java9之后不建议使用这个构造方法了。出现横线表示已过时。
// 将数字100转换成Integer包装类型(int --> Integer)
Integer x = new Integer(100);

// 将String类型的数字,转换成Integer包装类型。(String --> Integer)
Integer y = new Integer("123");

// double -->Double
Double d = new Double(1.23);

// String --> Double
Double e = new Double("3.14");

获取最大值最小值
最大值Integer.MAX_VALUE
最小值Integer.MIN_VALUE
其他类型也同理

public class IntegerTest04 {
    public static void main(String[] args) {
        // 通过访问包装类的常量,来获取最大值和最小值
        System.out.println("int的最大值:" + Integer.MAX_VALUE);
        System.out.println("int的最小值:" + Integer.MIN_VALUE);
        System.out.println("byte的最大值:" + Byte.MAX_VALUE);
        System.out.println("byte的最小值:" + Byte.MIN_VALUE);
    }
}

string转换成数字static int parseInt(String s)

// 静态方法,传参String,返回int
//网页上文本框中输入的100实际上是"100"字符串。后台数据库中要求存储100数字,此时java程序需要将"100"转换成100数字。
int retValue = Integer.parseInt("123"); // String -转换-> int

同样其他类型参数也有类似的方法

// 照葫芦画瓢
double retValue2 = Double.parseDouble("3.14");
System.out.println(retValue2 + 1); //4.140000000000001(精度问题)

float retValue3 = Float.parseFloat("1.0");
System.out.println(retValue3 + 1); //2.0

在这里插入图片描述

public class IntegerTest08 {
    public static void main(String[] args) {

        // String --> int
        int i1 = Integer.parseInt("100"); // i1是100数字
        System.out.println(i1 + 1); // 101

        // int --> String
        String s2 = i1 + ""; // "100"字符串
        System.out.println(s2 + 1); // "1001"

        // int --> Integer
        // 自动装箱
        Integer x = 1000;

        // Integer --> int
        // 自动拆箱
        int y = x;

        // String --> Integer
        Integer k = Integer.valueOf("123");

        // Integer --> String
        String e = String.valueOf(k);
    }
}

3.4 日期相关类

  • 获取当前日期
  • String转换成日期格式
  • 日期格式转换成String

获取对象之后,通过对象调取format格式的方法

// 获取系统当前时间(精确到毫秒的系统当前时间)
// 直接调用无参数构造方法就行。
Date nowTime = new Date();

System.out.println(nowTime); //Thu Mar 05 10:51:06 CST 2020

SimpleDateFormat是java.text包下的,专门负责日期格式化的
具体参数

代码参数代表含义
yyyy年(年是4位)
MM月(月是2位)
dd
HH
mm
ss
SSS毫秒(毫秒3位,最高999。1000毫秒代表1秒)

在日期格式中,除了y M d H m s S这些字符不能随便写之外,剩下的符号格式自己随意组织。

Date nowTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String nowTimeStr = sdf.format(nowTime);
System.out.println(nowTimeStr);

代码截图如下
在这里插入图片描述
以上是日期转换string字符串
下面是字符串转换成日期格式

// String --> Date
String time = "2008-08-08 08:08:08 888";
//SimpleDateFormat sdf2 = new SimpleDateFormat("格式不能随便写,要和日期字符串格式相同");
// 注意:字符串的日期格式和SimpleDateFormat对象指定的日期格式要一致。不然会出现异常:java.text.ParseException
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
Date dateTime = sdf2.parse(time);
System.out.println(dateTime); //Fri Aug 08 08:08:08 CST 2008

获取1970到现在的毫秒数System.currentTimeMillis();

// 获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数。
long nowTimeMillis = System.currentTimeMillis();
System.out.println(nowTimeMillis); 

可以统计一个方法所执行耗费的时长

// 在调用目标方法之前记录一个毫秒数
long begin = System.currentTimeMillis();
//方法;
// 在执行完目标方法之后记录一个毫秒数
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");

补充System的方法

  • System.gc() 建议启动垃圾回收器
  • System.exit(0) 退出JVM

讲完Date无参构造函数,下文讲有参构造函数

// 1970-01-01 00:00:00 001
Date time = new Date(1); // 注意:参数是一个毫秒

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(time);
// 北京是东8区。差8个小时。
System.out.println(strTime); // 1970-01-01 08:00:00 001


// 获取昨天的此时的时间。
Date time2 = new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24);
String strTime2 = sdf.format(time2);
System.out.println(strTime2); //2020-03-04 11:44:14 829

3.5 数字相关类

java.text.DecimalFormat专门负责数字格式化的。DecimalFormat df = new DecimalFormat("数字格式");
获取对象之后,通过对象调取format格式的方法
数字格式有:

  • #代表任意数字
  • , 代表千分位
  • . 代表小数点
  • 0 代表不够时补0

###,###.## 表示:加入千分位,保留2个小数

DecimalFormat df = new DecimalFormat("###,###.##");
//String s = df.format(1234.56); //"1,234.56"
String s = df.format(1234.561232);
System.out.println(s); // "1,234.56"

DecimalFormat df2 = new DecimalFormat("###,###.0000"); //保留4个小数位,不够补上0
String s2 = df2.format(1234.56);
System.out.println(s2); // "1,234.5600"

BigDecimal 属于大数据,精度极高。不属于基本数据类型,属于java对象(引用数据类型)

// 这个100不是普通的100,是精度极高的100
BigDecimal v1 = new BigDecimal(100);
// 精度极高的200
BigDecimal v2 = new BigDecimal(200);
// 求和
// v1 + v2; // 这样不行,v1和v2都是引用,不能直接使用+求和。
BigDecimal v3 = v1.add(v2); // 调用方法求和。
System.out.println(v3); //300

BigDecimal v4 = v2.divide(v1);
System.out.println(v4); // 2

3.6 Random类

产生随机数
先获取随机数的对象
通过对象获取随机取值的范围

// 创建随机数对象
Random random = new Random();

// 随机产生一个int类型取值范围内的数字。
int num1 = random.nextInt();

System.out.println(num1);

nextInt翻译为:下一个int类型的数据是101,表示只能取到100

// 产生[0~100]之间的随机数。不能产生101。
int num2 = random.nextInt(101); //不包括101
System.out.println(num2);

程序:生成随机5个数字不重复

public class RandomTest02 {
    public static void main(String[] args) {

        // 创建Random对象
        Random random = new Random();

        // 准备一个长度为5的一维数组。
        int[] arr = new int[5]; // 默认值都是0
        for(int i = 0; i < arr.length; i++){
            arr[i] = -1;
        }

        // 循环,生成随机数
        int index = 0;
        while(index < arr.length){
            // 生成随机数
            //int num = random.nextInt(101);
            //int num = random.nextInt(6); // 只能生成[0-5]的随机数!
            int num = random.nextInt(4); // 只能生成[0-3]的随机数!永远都有重复的,永远都凑不够5个。
            System.out.println("生成的随机数:" + num);
            // 判断arr数组中有没有这个num
            // 如果没有这个num,就放进去。
            if(!contains(arr, num)){
                arr[index++] = num;
            }
        }

        // 遍历以上的数组
        for(int i = 0; i < arr.length; i++){
            System.out.println(arr[i]);
        }
    }

    /**
     * 单独编写一个方法,这个方法专门用来判断数组中是否包含某个元素
     * @param arr 数组
     * @param key 元素
     * @return true表示包含,false表示不包含。
     */
    public static boolean contains(int[] arr, int key){
        /*
        // 这个方案bug。(排序出问题了。)
        // 对数组进行升序
        //Arrays.sort(arr);
        // 进行二分法查找
        // 二分法查找的结果 >= 0说明,这个元素找到了,找到了表示存在!
        //return Arrays.binarySearch(arr, key) >= 0;
         */

        for(int i = 0; i < arr.length; i++){
            if(arr[i] == key){
                // 条件成立了表示包含,返回true
                return true;
            }
        }
        // 这个就表示不包含!
        return false;
    }

}

3.7 enum类

  • 枚举:一枚一枚可以列举出来的,才建议使用枚举类型。
  • 编译之后也是生成class文件。
  • 一种引用数据类型。
  • 每一个值可以看做是常量
enum 枚举类型名{
    枚举值1,枚举值2
}

结果只有两种情况的,建议使用布尔类型
结果超过两种并且还是可以一枚一枚列举出来的,建议使用枚举类型
例如

/**
 * 四季枚举类型
 */
 
public enum Season {
    /*
    春夏秋冬
     */
    SPRING, SUMMER, AUTUMN, WINTER
}

具体结合的用法如下
高版本的JDK,switch支持int、String、枚举

public class SwitchTest {
    public static void main(String[] args) {
        // switch语句支持枚举类型
        // switch也支持String、int
        // 低版本的JDK,只支持int
        // 高版本的JDK,支持int、String、枚举。
        // byte short char也可以,因为存在自动类型转换。
        switch (Season.SPRING) {
            // 必须省略Season.
            case SPRING:
                System.out.println("春天");
                break;
            case SUMMER:
                System.out.println("夏天");
                break;
            case AUTUMN:
                System.out.println("秋天");
                break;
            case WINTER:
                System.out.println("冬天");
                break;
        }

    }
}

补充另外一个程序

enum Result{
    // SUCCESS 是枚举Result类型中的一个值
    // FAIL 是枚举Result类型中的一个值
    // 枚举中的每一个值,可以看做是“常量”
    SUCCESS, FAIL
}

通过这个可以设计函数
注意下面函数的区分
通过类调用枚举的属性

public class EnumTest02 {
    public static void main(String[] args) {
        Result r = divide(10, 2);
        System.out.println(r == Result.SUCCESS ? "计算成功" : "计算失败");
    }

    /**
     * 计算两个int类型数据的商。
     * @param a int数据
     * @param b int数据
     * @return Result.SUCCESS表示成功,Result.FAIL表示失败!
     */
    public static Result divide(int a, int b){
        try {
            int c = a / b;
            return Result.SUCCESS;
        } catch (Exception e){
            return Result.FAIL;
        }
    }
}

4.异常处理

异常的处理机制可结合我上一篇的文章
java异常处理机制

本文阐述的异常处理也比较全,两者选其一也可

异常的作用:增强程序的健壮性

通过异常类创建对象
因为异常在java中以类的形式存在,每一个异常类都可以创建异常对象

// 通过“异常类”实例化“异常对象”
NumberFormatException nfe = new NumberFormatException("数字格式化异常!");

// java.lang.NumberFormatException: 数字格式化异常!
System.out.println(nfe);

// 通过“异常类”创建“异常对象”
NullPointerException npe = new NullPointerException("空指针异常发生了!");

//java.lang.NullPointerException: 空指针异常发生了!
System.out.println(npe);

编译时异常和运行时异常

  • 都是发生在运行阶段。编译阶段异常是不会发生的
  • 只有程序运行阶段才可以new对象。因为异常的发生就是new异常对象
Object Throwable Error Exception VitualMachineError IOError StackOverflowError ExceptionSubclass RuntimeException NullPointerException ClassCastException illegalArgumentException NumberFormatException Exception的直接子类
  • 不管是错误还是异常,都是可以抛出的
  • 所有错误只要发生,java程序终止程序执行,退出jvm,错误是不能处理的
  • 编译时异常:所有Exception的直接子类,都可以叫做是编译时的异常,表示必须在编写程序时候预先对这种异常进行处理,如果不处理编译器会出错。发生概率比较高。也叫受检异常(CheckedException)或者受控异常
  • 运行时异常:所有RuntimeException及子类都属于运行时异常,运行时异常可处理也可不处理,发生概率比较低。叫未受检异常(UnCheckedException)或者非受控异常

4.1 异常处理方式

  • 在方法声明的位置上,使用throws关键字
  • 使用try…catch语句进行异常的捕捉

注意:Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果。终止java程序的执行

public class ExceptionTest03 {
    public static void main(String[] args) {
        /*
        程序执行到此处发生了ArithmeticException异常,
        底层new了一个ArithmeticException异常对象,
        然后抛出了,由于是main方法调用了100 / 0,
        所以这个异常ArithmeticException抛给了main方法,
        main方法没有处理,将这个异常自动抛给了JVM。
        JVM最终终止程序的执行。

        ArithmeticException 继承 RuntimeException,属于运行时异常。
        在编写程序阶段不需要对这种异常进行预先的处理。
         */
        System.out.println(100 / 0);

        // 这里的HelloWorld没有输出,没有执行。
        System.out.println("Hello World!");
    }
}

异常处理的原理:
通过抛出异常往上捕捉

public class ExceptionTest04 {
    public static void main(String[] args) {
    //doSome();
    //如果不加dosome函数编译通过,如果加上就会出错,必须对方法异常进行预处理
    
    }
    
    public static void doSome() throws ClassNotFoundException{
        System.out.println("doSome!!!!");
    }

}

第一种方法:
在方法声明的位置上继续使用:throws,来完成异常的继续上抛。抛给调用者。上抛类似推卸责任,将其抛给上一级完成

public static void main(String[] args) throws ClassNotFoundException {
        doSome();
}

第二种方法:
try…catch进行捕捉
下抛是进行捕捉完成

public static void main(String[] args) {
    try {
        doSome();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

4.2 异常捕捉和上报联合使用

  • 只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行,try…catch捕捉异常之后,后续代码可以执行
  • 如果希望调用者来处理,选择throws上报。其它情况使用捕捉的方式
  • 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。一般main方法中的异常建议使用try…catch进行捕捉。main就不要继续上抛了
  • 抛出异常可以多个种类或者是其中一个子类也可以
FileInputStream fis = new FileInputStream("文件名");

通常调用这个类会执行错误,本身这个类加油thros抛出异常,需要进一步上抛出或者截获使用try catch语句
此处主要讲解上抛的结构

public class ExceptionTest06 {

    /*
    public static void main(String[] args) throws FileNotFoundException {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }
     */
    public static void main(String[] args) {

        System.out.println("main begin");
        try {
            // try尝试
            m1();
            // 以上代码出现异常,直接进入catch语句块中执行。
            System.out.println("hello world!");
        } catch (FileNotFoundException e){ // catch后面的好像一个方法的形参。
            // 这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址。
            // catch是捕捉异常之后走的分支。
            // 在catch分支中干什么?处理异常。
            System.out.println("文件不存在,可能路径错误,也可能该文件被删除了!");
            System.out.println(e); //java.io.FileNotFoundException: D:\course\01-课\学习方法.txt (系统找不到指定的路径。)
        }

        // try..catch把异常抓住之后,这里的代码会继续执行。
        System.out.println("main over");
    }

    private static void m1() throws FileNotFoundException {
        System.out.println("m1 begin");
        m2();
        // 以上代码出异常,这里是无法执行的。
        System.out.println("m1 over");
    }

    // 抛别的不行,抛ClassCastException说明你还是没有对FileNotFoundException进行处理
    //private static void m2() throws ClassCastException{
    // 抛FileNotFoundException的父对象IOException,这样是可以的。因为IOException包括FileNotFoundException
    //private static void m2() throws IOException {
    // 这样也可以,因为Exception包括所有的异常。
    //private static void m2() throws Exception{
    // throws后面也可以写多个异常,可以使用逗号隔开。
    //private static void m2() throws ClassCastException, FileNotFoundException{
    private static void m2() throws FileNotFoundException {
        System.out.println("m2 begin");
        // 编译器报错原因是:m3()方法声明位置上有:throws FileNotFoundException
        // 我们在这里调用m3()没有对异常进行预处理,所以编译报错。
        // m3();

        m3();
        // 以上如果出现异常,这里是无法执行的!
        System.out.println("m2 over");
    }

    private static void m3() throws FileNotFoundException {
        // 调用SUN jdk中某个类的构造方法。
        // 这个类还没有接触过,后期IO流的时候就知道了。
        // 我们只是借助这个类学习一下异常处理机制。
        // 创建一个输入流对象,该流指向一个文件。
        /*
        编译报错的原因是什么?
            第一:这里调用了一个构造方法:FileInputStream(String name)
            第二:这个构造方法的声明位置上有:throws FileNotFoundException
            第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是Exception,
            最终得知,FileNotFoundException是编译时异常。

            错误原因?编译时异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就报错。
         */
        //new FileInputStream("D:\\course\\01-开课\\学习方法.txt");

        // 我们采用第一种处理方式:在方法声明的位置上使用throws继续上抛。
        // 一个方法体当中的代码出现异常之后,如果上报的话,此方法结束。
        new FileInputStream("D:\\course\\01-课\\学习方法.txt");

        System.out.println("如果以上代码出异常,这里会执行吗??????????????????不会!!!");
    }
}

4.3 try catch深入

上面的文章中进行上抛的异常
此处讲解截获使用try catch结构

FileInputStream fis = new FileInputStream("文件名");

catch结构截获解决异常
例如

try {
    FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
    System.out.println("以上出现异常,这里无法执行!");
} catch(FileNotFoundException e) {
    System.out.println("文件不存在!");
}

catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型
此处可以用FileNotFoundException e

  • 也可以用IOException e,因为多态:IOException e = new FileNotFoundException();
  • 也可以用Exception e,因为多态:Exception e = new FileNotFoundException();

catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试

try {
    //创建输入流
    FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
    //读文件
    fis.read();
} catch(FileNotFoundException e) {
    System.out.println("文件不存在!");
} catch(IOException e){
    System.out.println("读文件报错了!");
}

jdk8以上之后可以写或语句

// JDK8的新特性!
try {
    //创建输入流
    FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
    // 进行数学运算
    System.out.println(100 / 0); // 这个异常是运行时异常,编写程序时可以处理,也可以不处理。
} catch(FileNotFoundException | ArithmeticException | NullPointerException e) {
    System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}

catch写多个的时候,从上到下,必须遵守从小到大

// 编译报错。
try {
    //创建输入流
    FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
    //读文件
    fis.read();
} catch(IOException e){
    System.out.println("读文件报错了!");
} catch(FileNotFoundException e) {
    System.out.println("文件不存在!");
}

4.4 异常对象常用方法

  • 获取异常简单的描述信息:String msg = exception.getMessage();

  • 打印异常追踪的堆栈信息:exception.printStackTrace();java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的

public class ExceptionTest08 {
    public static void main(String[] args) {

        // 这里只是new了异常对象,但是没有将异常对象抛出。JVM会认为这是一个普通的java对象。因为此处的对象源代码没有throws上抛
        NullPointerException e = new NullPointerException("空指针异常fdsafdsafdsafds");

        // 获取异常简单描述信息:这个信息实际上就是构造方法上面String参数。
        String msg = e.getMessage(); //空指针异常fdsafdsafdsafds
        System.out.println(msg);

        // 打印异常堆栈信息
        e.printStackTrace();

        for(int i = 0; i < 1000; i++){
            System.out.println("i = " + i);
        }

        System.out.println("Hello World!");
    }
}

所谓异步线程,是控制台如果有其他输出也会跟着一起输出

try {
    m1();
} catch (FileNotFoundException e) {
    // 获取异常的简单描述信息
    String msg = e.getMessage();
    System.out.println(msg); //C:\jetns-agent.jar (系统找不到指定的文件。)

    //打印异常堆栈追踪信息!!!
    //在实际的开发中,建议使用这个。养成好习惯!
    // 这行代码要写上,不然出问题你也不知道!
    e.printStackTrace();
}

异常追踪信息如图所示
在这里插入图片描述
只需要看本身写的文件bug出错即可
59行问题导致在55行,依次类推
追踪的信息最终在19行,只需要看19行即可

4.5 finally子句

  • 在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常,finally子句必须和try一起出现,不能单独编写
  • 通常用的情况:通常在finally语句块中完成资源的释放/关闭,因为finally中的代码比较有保障。即使try语句块中的代码出现异常,finally中代码也会正常执行

在try中定义的对象不能在finally使用,所以要想使用必须设置为全局变量

public class ExceptionTest10 {
    public static void main(String[] args) {
        FileInputStream fis = null; // 声明位置放到try外面。这样在finally中才能用。
        try {
            // 创建输入流对象
            fis = new FileInputStream("D:\\course\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
            // 开始读文件....

            String s = null;
            // 这里一定会出现空指针异常!
            s.toString();
            System.out.println("hello world!");

            // 流使用完需要关闭,因为流是占用资源的。
            // 即使以上程序出现异常,流也必须要关闭!
            // 放在这里有可能流关不了。
            //fis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch(IOException e){
            e.printStackTrace();
        } catch(NullPointerException e) {
            e.printStackTrace();
        } finally {
            System.out.println("hello 浩克!");
            // 流的关闭放在这里比较保险。
            // finally中的代码是一定会执行的。
            // 即使try中出现了异常!
            if (fis != null) { // 避免空指针异常!
                try {
                    // close()方法有异常,采用捕捉的方式。
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println("hello kitty!");

    }
}

子句中注意事项

  • try不能单独使用
  • try finally可以联合使用,可以没有catch
  • 代码执行顺序,先执行try,在finally,在执行try中涉及的return
try {
    System.out.println("try...");
    return;
} finally {
    // finally中的语句会执行。能执行到。
    System.out.println("finally...");
}

// 这里不能写语句,因为这个代码是无法执行到的。
//System.out.println("Hello World!");
  • 在try中有exit,退出JVM之后,finally语句中的代码就不执行了
try {
    System.out.println("try...");
    // 退出JVM
    System.exit(0); // 退出JVM之后,finally语句中的代码就不执行了!
} finally {
    System.out.println("finally...");
}

4.6 final、finally与finalize区别

  • final是一个关键字。表示最终的。不变的
  • finally也是一个关键字,和try联合使用,使用在异常处理机制中
  • finalize()是Object类中的一个方法。作为方法名出现,finalize是标识符。这个方法是由垃圾回收器GC负责调用的

4.7 自定义异常

  • 编写一个类继承Exception或者RuntimeException
  • 提供两个构造方法,一个无参数的,一个带有String参数的
    (具体灵感来源源码)
public class MyException extends Exception{ // 编译时异常
    public MyException(){

    }
    public MyException(String s){
        super(s);
    }
}

/*
public class MyException extends RuntimeException{ // 运行时异常

}
 */

正式调用异常的类以及使用异常常用方法

public class ExceptionTest15 {
    public static void main(String[] args) {

        // 创建异常对象(只new了异常对象,并没有手动抛出)
        MyException e = new MyException("用户名不能为空!");

        // 打印异常堆栈信息
        e.printStackTrace();

        // 获取异常简单描述信息
        String msg = e.getMessage();
        System.out.println(msg);
    }
}

4.8 面试题

补充:

  • throws 在方法声明位置上使用,表示上报异常信息给调用者
  • throw 手动抛出异常

代码自上而下执行,与上面的结构有些违和,最后的输出是100而不是101

public static void main(String[] args) {
    int result = m();
    System.out.println(result); //100
}

/*
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):
    java中有一条这样的规则:
        方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
    java中海油一条语法规则:
        return语句一旦执行,整个方法必须结束(亘古不变的语法!)
 */
public static int m(){
    int i = 100;
    try {
        // 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100
        // return语句还必须保证是最后执行的。一旦执行,整个方法结束。
        return i;
    } finally {
        i++;
    }
}

反编译之后的语句是,而且也满足从上往下的执行代码顺序

public static int m(){
    int i = 100;
    int j = i;
    i++;
    return j;
}

重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少
父类抛出异常,子类可以抛出也可以不抛出或者抛出同类似类即可
父类没抛出异常,子类不可以抛出异常但可以重写覆盖RuntimeException的运行时异常

class Animal {
    public void doSome(){

    }

    public void doOther() throws Exception{

    }
}

class Cat extends Animal {

    // 编译正常。
    public void doSome() throws RuntimeException{

    }

    // 编译报错。
    /*public void doSome() throws Exception{

    }*/

    // 编译正常。
    /*public void doOther() {

    }*/

    // 编译正常。
    /*public void doOther() throws Exception{

    }*/

    // 编译正常。
    public void doOther() throws NullPointerException{

    }
}

4.9 实际开发场景

用户业务类,处理用户相关的业务:例如登录、注册等功能
实现的用户类

public class UserService {

    /**
     * 用户注册
     * @param username 用户名
     * @param password 密码
     * @throws IllegalNameException 当用户名为null,或者用户名长度小于6,或者长度大于14,会出现该异常!
     */
    public void register(String username, String password) throws IllegalNameException {
        /*
        引用等于null的这个判断最好放到所有条件的最前面。
         */
        /*if(username == null || username.length() < 6 || username.length() > 14){
        }*/

        /*
        再分享一个经验:username == null 不如写成 null == username
        "abc".equals(username) 比 username.equals("abc") 好。
         */
        /*if(null == username || username.length() < 6 || username.length() > 14){
        }*/

        if(null == username || username.length() < 6 || username.length() > 14) {
            /*System.out.println("用户名不合法,长度必须在[6-14]之间");
            return;*/
            throw new IllegalNameException("用户名不合法,长度必须在[6-14]之间");
        }
        // 程序能够执行到此处说明,用户名合法
        System.out.println("注册成功,欢迎["+username+"]");
    }
}

自定义异常

public class IllegalNameException extends Exception {
    public IllegalNameException(){

    }
    public IllegalNameException(String s){
        super(s);
    }
}

通过测试类检测

public class Test {
    public static void main(String[] args) {
        // 创建UserService对象
        UserService userService = new UserService();
        // 用户名和密码就不再从控制台接收了
        try {
            userService.register("jack", "123");
        } catch (IllegalNameException e) {
            //System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }
}

在此强调

这部分知识一共有3个文档
第二个是当前这个文档

  1. java零基础从入门到精通(全)
  2. javaSE从入门到精通的十万字总结(一)
  3. javaSE从入门到精通的十万字总结(完结)
  • 18
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农研究僧

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值