抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

JDK8官方文档查看:https://docs.oracle.com/javase/8/docs/api/

一、Java基础语法

1.1 数据类型

Java是强类型语言,要求变量的使用要严格符合规定,所有变量必须定义后才能使用

Java的数据类型分为两大类:基本类型和引用类型

位(bit):是计算机内部数据储存的最小单位

字节(byte):是计算机中数据处理的基本单位,习惯用大写B表示

1B=8bit,1024B=1KB,1024KB=1M,1024M=1G

1.1.1 基本数据类型

基本数据类型又分为数值类型和boolean类型

数值类型

整数类型

  • byte占1个字节范围:-128~127

  • short占2个字节范围:-32768~32767

  • int占4个字节范围:-2147483648~2147483647

  • long占8个字节范围

浮点类型

  • float占4个字节
  • double占8个字节

字符类型

  • char占2个字节

注:字符串String不是关键字,是类;最好完全使用浮点数进行比较

boolean类型

  • 占1个字节,值只有true和false两个

1.1.2 引用数据类型

引用数据类型分为类、接口、数组

1.1.3 类型转换

运算中,不同类型的数据先转换为同一类型,然后进行运算

数据类型范围小的数据转化为数据类型范围大的数据不需要进行强制转换;范围大的转化为范围小的需要进行强制转换,但是数据精度可能会有丢失

即,

注:不能对布尔值进行转换;不能把对象类型转换为不相干的类型;转换的时候可能出现内存溢出或者精度问题

1.1.4 赋值问题

操作比较大的数的时候,需要注意溢出问题

int money = 1000000000;
int year = 20;
int total = money*year; //此时的total已经溢出,输出的结果就不是20000000000
long total2 = money*year; //此时输出total2的值也是错的,因为money*year作为int类型赋值给long类型的total2之前得到的值就已经出现了问题,所以应该在计算之前就进行转换,即第三种
long total3 = (long)money*year; //此时因为将money强制转化为long类型,所以运算是按照数据范围更大的long类型进行运算,所以此时输出total3是正确的值;money*(long)year也行

1.2 变量

1.2.1 变量声明

Java是一种强类型语言,每个变量都必须声明其类型

Java变量是程序中最基本的存储单元,其要素包括变量名,变量类型和作用域

type varName[=value] [{,varName[=value]}] ;
//数据类型变量名=值;可以使用逗号隔开来声明多个同类型变量。
int a=1,b=2,c=3;
String name = "xk";
char x = 'x';
double pi = 3.14;
float f = 2.454f;
long L = 34532847L;

注:每个变量都有类型,类型可以是基本类型,也可以是引用类型;变量名必须是合法的标识符;变量声明是一条完整的语句,因此每一个声明都必须以分号结束。

1.2.2 变量分类及作用域

变量分为类变量、实例变量、局部变量

局部变量

①局部变量声明在方法、构造方法或者语句块中

②局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁

访问修饰符不能用于局部变量

④局部变量只在声明它的方法、构造方法或者语句块中可见

⑤局部变量是在栈上分配的

⑥局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用

public class Test{ 
   public void pupAge(){
      int age = 0;
      age = age + 7;
      System.out.println("小狗的年龄是: " + age);
   }
   
   public static void main(String[] args){
      Test test = new Test();
      test.pupAge();
   }
}
实例变量

①实例变量声明在一个类中,但在方法、构造方法和语句块之外

②当一个对象被实例化之后,每个实例变量的值就跟着确定

③实例变量在对象创建的时候创建,在对象被销毁的时候销毁

实例变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息

实例变量可以声明在使用前或者使用后

访问修饰符可以修饰实例变量

实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有,通过使用访问修饰符可以使实例变量对子类可见

实例变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是false,引用类型变量的默认值是null。变量的值可以在声明时指定,也可以在构造方法中指定

⑨实例变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObejectReference.VariableName(对象名.变量名)

public class Employee{
   // 这个实例变量对子类可见
   public String name;
   // 私有变量,仅在该类可见
   private double salary;
   //在构造器中对name赋值
   public Employee (String empName){
      name = empName;
   }
   //设定salary的值
   public void setSalary(double empSal){
      salary = empSal;
   }  
   // 打印信息
   public void printEmp(){
      System.out.println("名字 : " + name );
      System.out.println("薪水 : " + salary);
   }
 
   public static void main(String[] args){
      Employee empOne = new Employee("RUNOOB");
      empOne.setSalary(1000.0);
      empOne.printEmp();
   }
}
类变量(静态变量)

①类变量也称为静态变量,在类中以 static 关键字声明,但必须在方法之外

无论一个类创建了多少个对象,类只拥有类变量的一份拷贝(所有对象共享一份静态变量)

③静态变量除了被声明为常量外很少使用,静态变量是指声明为public/private,final 和 static 类型的变量。静态变量初始化后不可改变

静态变量储存在静态存储区。经常被声明为常量,很少单独使用 static 声明变量

静态变量在第一次被访问时创建,在程序结束时销毁

⑥与实例变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为 public 类型

默认值和实例变量相似。数值型变量默认值是 0,布尔型默认值是 false,引用类型默认值是 null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化

⑧静态变量可以通过:ClassName.VariableName(类名.变量名)的方式访问,也可以通过对象名.静态变量进行访问但java不推荐这样做

类变量被声明为 public static final 类型时,类变量名称一般建议使用大写字母。如果静态变量不是 public 和 final 类型,其命名方式与实例变量以及局部变量的命名方式一致

public class Employee {
    //salary是静态的私有变量
    private static double salary;
    // DEPARTMENT是一个常量
    public static final String DEPARTMENT = "开发人员";
    public static void main(String[] args){
    salary = 10000;
        System.out.println(DEPARTMENT+"平均工资:"+salary);
    }
}
//注:如果其他类想要访问该变量,可以这样访问:Employee.DEPARTMENT

1.2.3 常量

常量(Constant):初始化(initialize)后不能再改变值!不会变动的值

所谓常量可以理解成一种特殊的变量,它的值被设定后,在程序运行过程中不允许被改变。

final 常量名=值;
final double PI=3.14;

常量名一般使用大写字符,final是指该变量只准被修改一次

1.3 运算符

Java支持的运算符:

  • 算术运算符:+、-、*、/、%、++、–
  • 赋值运算符:=
  • 关系运算符:>、<、>=、<=、==、!=、instanceof
  • 逻辑运算符:&&、||、!
  • 位运算符:&、|、^、~、>>、<<、>>>(了解)
  • 条件运算符(三元运算符):?:
  • 扩展赋值运算符:+=、-=、*=、/=

1.3.1 自增(++)、自减(–)运算符

int a = 3;
int b = a++; //先给b赋值,再自增
int c = ++a; //先自增,再给b赋值

1.3.2 逻辑运算符、位运算符

逻辑运算符:与(and)、或(or)、非(!)

逻辑与运算、逻辑或运算又分为短路与(&&)、长路与(&)、短路或(||)、长路或(|)

区别:

短路与和长路与:长路与 两侧都会被运算;短路与 只要第一个是false,第二个就不会进行运算了

短路或和长路或:长路或 两侧都会被运算;短路或 只要第一个是true,第二个就不会进行运算了

//与(and)   或(or)   非(取反)
boolean a = true;
boolean b = false;

System.out.println(a&&b); //与运算,都为真结果才能为真
System.out.println(a||b); //或运算,其中一个为真,结果就为真
System.out.println(!(a&&b)); //如果a&&b是真,结果为假;否则为真

位运算符:长路与(&)、长路或(|)、异或(^)、非(~)、左移(<<)、右移(>>)

//位运算,效率极高!!

A = 0011 1100;
B = 0000 1101;

A&B = 0000 1100; //与,都为1才为1
A|B = 0011 1101; //或,其中一个为1就为1
A^B = 1100 1110; //异或运算,相同为真,不同为假
~B = 1111 0010; //位取反

/*  2*8=16   2*2*2*2
    <<   相当于*,根据一个整数的二进制表达,将其每一位都向左移动,最右边补0
    >>   相当于/,根据一个整数的二进制表达,将其每一位都向右移动     
    0000 0010    2
    0000 1000    8
    0001 0000    16
*/
System.out.println(2<<3);  //结果为16,即将2的二进制表达的每一位向左移动3位,也相当于2*2*2*2

1.3.3 三元运算符、字符串连接符

//字符串连接符  +
int a = 10;
int b = 20;
System.out.println("a+b"+a+b); //输出a+b1020,因为a+b在String类型的"a+b"之后,会将a和b的值直接在其后面进行拼接
System.out.println(a+b+"a+b"); //输出30a+b,因为a+b在String类型的"a+b"之前,会先将a+b进行运算后再拼接


//三元运算符  x ? y : z
//如果x=true,结果为y;如果x=false,结果为z

1.3.4 运算符优先级

所有的数学运算都认为是从左向右运算的,Java 语言中大部分运算符也是从左向右结合的,只有单目运算符、赋值运算符和三目运算符例外,其中,单目运算符、赋值运算符和三目运算符是从右向左结合的,也就是从右向左运算

乘法和加法是两个可结合的运算,也就是说,这两个运算符左右两边的操作数可以互换位置而不会影响结果。运算符有不同的优先级,所谓优先级就是在表达式运算中的运算顺序

一般而言,单目运算符优先级较高,赋值运算符优先级较低。算术运算符优先级较高,关系和逻辑运算符优先级较低。多数运算符具有左结合性,单目运算符、三目运算符、赋值运算符具有右结合性

Java 语言中运算符的优先级共分为 14 级,其中 1 级最高,14 级最低。在同一个表达式中运算符优先级高的先执行。表 1 列出了所有的运算符的优先级以及结合性

运算符优先级:

优先级 运算符 结合性
1 ()、[]、{} 从左向右
2 !、+、-、~、++、– 从右向左
3 *、/、% 从左向右
4 +、- 从左向右
5 «、»、>>> 从左向右
6 <、<=、>、>=、instanceof 从左向右
7 ==、!= 从左向右
8 & 从左向右
9 ^ 从左向右
10 | 从左向右
11 && 从左向右
12 || 从左向右
13 ?: 从右向左
14 =、+=、-=、*=、/=、&=、|=、^=、~=、«=、»=、>>>= 从右向左

使用优先级为 1 的小括号可以改变其他运算符的优先级,即如果需要将具有较低优先级的运算符先运算,则可以使用小括号将该运算符和操作符括起来

二、Java流程控制

2.1 Scanner对象

java.util.Scanner是Java5的新特征,我们可以通过Scanner类来获取用户的输入。

基本语法:

Scanner s = new Scanner(System.in);

通过Scanner类的next()与nextLine()方法获取输入的字符串,在读取前我们一般需要使用hasNext()与hasNextLine()判断是否还有输入的数据

next():

1、一定要读取到有效字符后才可以结束输入。

2、对输入有效字符之前遇到的空白,next()方法会自动将其去掉

3、只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。

4、next()不能得到带有空格的字符串。

nextLine():

1、以Enter为结束符,也就是说nextLine()方法返回的是输入回车之前的所有字符。

2、可以获得空白。

2.2顺序结构

Java本身执行代码的结构就是顺序结构,除非特别指明,否则就是按照顺序一句一句执行代码;它是由若干个一次执行的处理步骤组成的,它是任何一个算法都离不开的一种基本算法结构

2.3选择结构

2.3.1 if单选择结构

if(布尔表达式) {
    如果布尔表达式为真,则执行的代码语句
}

2.3.2 if…else双选择结构

if(布尔表达式) {
    如果布尔表达式为真,则执行的代码语句
} else {
    如果布尔表达式为假,则执行的代码语句
}

2.3.3 if…else if…else多选择结构

if(布尔表达式1) {
    如果布尔表达式1为真,则执行的代码语句
} else if(布尔表达式2) {
    如果布尔表达式2为真,则执行的代码语句
} else {
    如果上面的布尔表达式都不为真,则执行的代码语句
}
//只要有一个else if的布尔表达式为true,就会跳过其余所有的else if和else

2.3.4 嵌套的if选择结构

if(布尔表达式1) {
    如果布尔表达式1为真,则执行的代码语句
    if(布尔表达式2) {
    如果布尔表达式2为真,则执行的代码语句
    }     
} 
//也就是说可以另一个if或者else if里面使用if或else if语句;可以像if语句那样嵌套eles if ...else

2.3.5 Switch多选择结构

switch(expression) {  
    case value : 
        //语句
        break;//可选
    case value :
        //语句
        break;//可选
    default : //可选
        //语句
}
//expression变量类型可以是byte、short、int或者char;从Java SE7开始,switch支持String类型了;同时case标签必须为字符串常量或字面量

2.4 循环结构

循环结构分为while循环结构、do…while结构、for循环结构,JDK5中引入了一种主要用于数组的增强型for循环

2.4.1 while循环结构

while(布尔表达式) {
    //需要先判断布尔表达式再执行语句。只要布尔表达式为true,就会一直执行;要使其停止循环需要改变布尔表达式的值
}

2.4.2 do…while循环结构

do {
    //先执行一次循环体语句,再判断布尔表达式
} while(布尔表达式)

2.4.3 for循环结构

for循环语句是支持迭代的一种通用结构,是最有效、最灵活的循环结构

for循环执行的次数是在执行前就确定的

for(初始化循环变量; 布尔表达式 ; 更新循环变量) {
    //代码语句
}

//打印九九乘法表
for (int i=1;i<10;i++) {
            for(int j=1;j<=i;j++) {
                System.out.print(i+"*"+j+"="+i*j);
                System.out.print("   ");
            }
            System.out.println();
        }
//打印三角形
for(int i=1;i<=5;i++) { //打印第i行
    for(int j=5;j>=i;j--) { //第i行首先打印j-i+1个空格
        System.out.print(" ");
    }
    for(int j=1;j<=i;j++) { //第i行再打印一个*号
        System.out.print("*");
    }
    for(int j=1;j<i;j++) { //最后第i行打印i-1个*号
        System.out.print("*");
    }
    System.out.println(); //打印完一行换行
}

2.4.4 增强for循环

JDK5引入了一种主要应用于数组的增强for循环

for(声明语句 : 表达式) {
    //代码语句
}
//声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块,其值与此时数组元素的值相等。
//表达式:表达式是要访问的数组名,或者是返回值为数组的方法。
int[] numbers = {10,20,30,40,50};
for(int x:numbers) {
    System.out.println(x);
}

2.4.5 break、continue、goto

break:break在任何循环语句的主体部分,均可用break控制循环的流程。break用于强行退出循环,不执行循环中剩余的语句(break语句也在switch语句中使用)

continue:continue语句用在循环语句体中,用于终止某次循环过程,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判定

goto:

  • goto关键字很早就在程序设计语言中出现。尽管goto仍是Java的一个保留字,但并未在语言中得到正式使用;Java没有goto。然而,在break和continue这两个关键字的身上,我们仍然能看出一些goto的影子—带标签的break和continue.

  • “标签”是指后面跟一个冒号的标识符,例如: label:

  • 对Java来说唯一用到标签的地方是在循环语句之前。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环,由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方

三、Java方法

Java方法是语句的集合,它们在一起执行一个功能。

  • 方法是解决一类问题的步骤的有序组合

  • 方法包含于类或对象中

  • 方法在程序中被创建,在其他地方被引用

设计方法的原则:方法的本意是功能块,就是实现某个功能的语句块的集合。我们设计方法的时候,最好保持方法的原子性,就是一个方法只完成1个功能,这样利于我们后期的扩展

3.1 方法的定义和调用

3.1.1 方法的定义

Java的方法类似于其它语言的函数,是一段用来完成特定功能的代码片段,一般情况下,定义一个方法包含以下语法:

方法包含一个方法头和一个方法体。下面是一个方法的所有部分:

  • 访问修饰符:这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型

    • Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限:

      • default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

      • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

      • public : 对所有类可见。使用对象:类、接口、变量、方法

      • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

      修饰符 当前类 同一包内 子孙类(同一包) 子孙类(不同包) 其他包
      public Y Y Y Y Y
      protected Y Y Y Y/N N
      default Y Y Y N N
      private Y N N N N
  • 返回值类型∶方法可能会返回值。returnValueType是方法返回值的数据类型;有些方法执行所需的操作,但没有返回值,在这种情况下,returnValueType是关键字void

  • 方法名:是方法的实际名称。方法名和参数表共同构成方法签名

  • 参数类型:参数像是一个占位符。当方法被调用时,传递值给参数这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数

    • 形式参数:在方法被调用时用于接收外界输入的数据
    • 实参:调用方法时实际传给方法的数据
  • 方法体:方法体包含具体的语句,定义该方法的功能。

修饰符 返回值类型 方法名(参数类型 参数名) {
    ...
    方法体
    ...
    return 返回值;
}

3.1.2 方法的调用

实例方法:

调用方法:无论方法与方法的调用是否在同一个类中,都是先声明定义一个对象,然后通过对象名.方法名(实参列表)进行调用

  • 当方法返回一个值时,方法调用通常被当做一个值
  • 当方法返回为void时,方法调用是一条语句
Test test = new Test();
//方法有返回值时,如下
int max = test.max(10,20);
//方法返回void时,如下
test.max(10,20);

类方法(静态方法):

调用方法:如果方法与方法调用在同一个类中发生,可通过方法名直接调用;如果方法与方法调用没有发生在同一个类中,需要通过类名.方法名(实参列表)进行调用

  • 方法返回值的情况与实例方法相同
class A {
    public static void test() {
        //代码块
    }
    test(); //方法和方法调用发生在同一个类中
}
class B {
    A.test(); //方法和方法调用发生在不同类中
}

3.1.3 形参和实参

形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。 形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。 因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外

实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值

Tips:
值传递调用过程中,只能把实参的值传递给形参,而不能把形参的值反向作用到实参上。在函数调用过程中,形参的值发生改变,而实参的值不会发生改变

引用传递调用的机制中,实际上是将实参引用的地址传递给了形参,所以任何发生在形参上的改变也会发生在实参变量上

值传递引用传递又是什么呢?参考3.1.4

3.1.4 值传递和引用传递(重点\难点)

参考链接:

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量

引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身

一般认为,java内的基础类型数据传递都是值传递. java中实例对象的传递是引用传递

值传递:

  • 指的是在方法的调用中,传递的参数是按照值的拷贝传递
  • 特点:传递的是值的拷贝,传递后就互不相关了

引用传递:

  • 指的是在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。
  • 特点:传递的是参数的引用值,也就是传递前和传递后都指向同一个引用(即同一个内存空间)

Tips:

  • “在Java里面参数传递都是按值传递”这句话的意思是:按值传递是传递的值的拷贝,按引用传递其实传递的是引用的地址值,所以统称按值传递

  • 在Java里面只有基本类型和按照直接使用双引号定义字符串方式:String str = “Java私塾”这种定义方式的String是按值传递,其它的都是按引用传递

3.2 方法的重载

重载就是在一个类中,有相同的函数名称,但形参不同的函数

方法的重载的规则:

  • 方法名称必须相同
  • 参数列表必须不同(个数不同、或类型不同、参数排列顺序不同等)
  • 方法的返回类型可以相同也可以不相同
  • 仅仅返回类型不同不足以成为方法的重载

实现理论:

方法名称相同时,编译器会根据调用方法的参数个数、参数类型等去逐个匹配,以选择对应的方法,如果匹配失败,则编译器报错。

3.3 命令行传递参数

在控制台的命令行进行编译.java文件时传参

public class Test {
    public static void main(String[] args)
    for(int i = 0; i < args.length; i++) {
        System.out.println("args[" + i + "]:" + args[i]);
    }
}
//如果在命令行编译成功后输入命令:java Test this is test
//则运行程序时命令行会输出:args[0]:this        args[1]:is        args[2]:test

3.4 可变参数

JDK5开始,Java支持传递同类型的可变参数给一个方法

在方法声明中,在指定参数类型后加一个省略号(…)

一个方法只能制定一个可变参数,它必须是方法的最后一个参数。任何参数必须在它之前声明

test(1,3,4); //可以随意传n个参数
test(new Double[]{1,2,3});
public void test(double... numbers) {
    //方法体
}

3.4 递归

tips:深度较大的时候运行速度太慢,方便程序员,劳累电脑

递归就是:A方法调用A方法!就是自己调用自己

利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合

递归结构包括两个部分:

  • 递归头:什么时候不调用自身方法。如果没有头,将陷入死循环
  • 递归体:什么时候需要调用自身方法
//计算阶乘
public static void main(String[] args) {
        System.out.println(test(5));
    }
    public static int test(int i) {
        if(i == 1) {
            return 1; //递归头(尽头)
        } else {
            return i*test(i-1);
        }
    }

如图:

四、Java数组

数组是相同类型数据的有序集合

数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成

其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们

4.1 数组的声明创建

首先必须先声明数组变量,才能在程序中使用数组

dataType[] arrayRefVar; //首选方法
dataType arrayRefVar[]; //效果相同,但不是首选方法

//Java使用new操作符来创建数组
dataType[] arrayRefVar = new dataType[arraySize];

数组元素通过索引访问,数组索引从0开始

获取数组长度:arrays.length

4.2 Java内存分析及三种初始化

4.2.1 Java内存分析

先申请再创建

4.2.2 三种初始化方式

静态初始化

int[] a = {1,2,3};
Man[] mans = {new Man(1,1),new Man(2,2)};

动态初始化(包括默认初始化)

int[] a = new int[2];
a[0] = 1;
a[1] = 2;

数组的默认初始化

数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化(比如int类型的默认值都为0)

4.2.3 下标越界问题

数组的四个基本特点

  • 其长度是确定的。数组一旦被创建,它的大小就是不可以改变的
  • 其元素必须是相同类型,不允许出现混合类型
  • 数组中的元素可以是任何数据类型,包括基本类型和引用类型
  • 数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中的

若对数组下标的操作超过数组声明创建时的下标,就会出现下标越界问题,报ArrayIndexOutOfBounds异常

4.3 数组的使用

for-each循环

int[] arrays = {1,2,3,4,5};
for (int array : arrays) {
    //执行语句
}

数组作方法入参

public static void printArray(int[] arrays) {
    //执行语句
}

数组作返回值

//反转数组
public static void main(String[] args) {
        int[] arrays = {1,2,3,4,5};
        for (int array : arrays) {
            System.out.print(array + "  ");
        }
        System.out.println();
        int[] results =  reverse(arrays);
        for (int result : results) {
            System.out.print(result + "  ");
        }
    }

    public static int[] reverse(int[] arrays) {
        int[] result = new int[arrays.length];
        for (int i = 0; i < arrays.length; i++) {
            result[i] = arrays[arrays.length-1-i];
        }
        return result;
    }

4.4 多维数组

多维数组可以看成是数组的数组,比如二维数组是一个特殊的一维数组,其每一个元素都是一个一维数组

如,二维数组:int[][] a = new int[2][5]; 可以看成一个二行五列的数组

二维数组的arrays.length输出的是第一层数组的长度,比如new int[2][5]的长度就是2;如果要得到第二层数组的长度,应表示为arrays[0].length

4.5 Arrays工具类

由于数组对象本身并没有什么方法可以供我们调用,但API中提供了一个数组的工具类java.util.Arrays供我们使用,从而可以对数据对象进行一些基本的操作(查看JDK帮助文档)

Arrays类中的方法都是static修饰的静态方法,在使用的时候可以直接使用类名进行调用,而”不用”使用对象来调用(注意:是”不用”而不是”不能”)

Arrays类具有以下常用功能:

  • 给数组赋值:通过fill方法
  • 对数组排序:通过sort方法,按升序
  • 比较数组:通过equals 方法比较数组中元素值是否相等
  • 查找数组元素:通过binarySearch方法能对排序好的数组进行二分查找法操作
  • 输出数组信息:toString方法

4.6 数组排序

冒泡排序是最出名的排序算法之一,总共八大排序方法。即直接插入排序、希尔排序、简单选择排序、堆排序、冒泡排序、快速排序、归并排序、桶排序/基数排序

冒泡排序:

  • 比较数组中,两个相邻的元素,如果第一个数比第二个数大,我们就交换他们的位置
  • 每一次比较,都会产生出一个最大,或者最小的数字
  • 下一轮则可以少一次排序
  • 依次循环,直到结束
//冒泡排序
for(int n=1;n<a.length;n++) { //第n轮排序
    for(int i=0;i<a.length-n;i++) { //调换位置,i<a.length-n是因为每次比较后都会产出一个最大或最小的数,那么下一次就不用再比较它
        if(a[i]>a[i+1]) {
            int temp=a[i];
            a[i]=a[i+1];
            a[i+1]=temp;
        }
    }
}

选择排序:

  • 选择数组中第i+1个数,依次与其后面的数进行比较,若比后面的数大就调换位置,这样每次都会将这轮比较最小的数放在最前面
//选择排序
for(int i=0;i<a.length;i++) { //第i+1轮排序
    for(int j=i+1;j<a.length;j++) { //调换位置
        if(a[j]<a[i]) {
            int temp=a[i];
            a[i]=a[j];
            a[j]=temp;
        }
    }
}

4.7 稀疏数组

例子:

private static int sum = 0; //统计原始数组中有多少个非0数
private static int[][] primary; //声明定义原始数组
private static int[][] sparse; //声明定义稀疏数组
private static int[][] recover; //声明定义恢复数组
public static void main(String[] args) {
    CreateArrays();
    CreateSparse();
    RecoverArrays();
}
//创建一个原始数组
public static void CreateArrays() {
    primary = new int[6][6];
    primary[0][3] = 2;
    primary[1][1] = 3;
    primary[3][5] = 5;
    primary[5][3] = 1;
    System.out.println("输出原始数组:");
    for (int[] ints : primary) {
        for (int anInt : ints) {
            System.out.print(anInt + "\t");
            if(anInt != 0) {
                sum++; //非0数个数加1
            }
        }
        System.out.println();
    }
    System.out.println("================");
}

//创建稀疏数组存储原始数组的数据
public static void CreateSparse() {
    int count = 0; //由于稀疏数组中一行存储的是原始数组中非0数的行、列、值,所以需要用到一个count来决定非0数存储在稀疏数组的第几行
    sparse = new int[sum+1][3];
    sparse[0][0] = primary.length; //存放primary的行数
    sparse[0][1] = primary[0].length; //存放primary的列数
    sparse[0][2] = sum; //存放primary中非0数的个数
    for (int i = 0; i < primary.length; i++) {
        for (int j = 0; j < primary[i].length; j++) {
            if (primary[i][j] != 0) { //循环遍历primary数组,将非0数的所在行列和值保存在sparse中
                count++;
                sparse[count][0] = i;
                sparse[count][1] = j;
                sparse[count][2] = primary[i][j];
            }
        }
    }
    System.out.println("输出稀疏数组:");
    System.out.println("行" + "\t" + "列" + "\t" + "值" + "\t");
    for (int[] ints : sparse) {
        for (int anInt : ints) {
            System.out.print(anInt + "\t");
        }
        System.out.println();
    }
    System.out.println("================");
}

//通过稀疏数组恢复原始数组
public static void RecoverArrays() {
    recover = new int[sparse[0][0]][sparse[0][1]]; //还原数组的行数等于稀疏数组的第一行第一列的数,列数等于稀疏数组第一行第二列的数
    for (int i = 1; i < sparse.length; i++) {
        recover[sparse[i][0]][sparse[i][1]] = sparse[i][2]; //遍历稀疏数组,通过获取到的行列和数将其填充到到还原数组中
    }
    System.out.println("输出还原数组:");
    for (int[] ints : recover) {
        for (int anInt : ints) {
            System.out.print(anInt + "\t");
        }
        System.out.println();
    }
}

五、Java面向对象

5.1 什么是面向对象

(面试:面向过程和面向对象的区别)

面向过程思想(线性思维):

  • 步骤清晰简单,第一步做什么,第二步做什么……
  • 面向过程适合处理一些较为简单的问题

面向对象思想:

  • 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。最后,才对某个分类下的细节进行面向过程的思索
  • 面向对象适合处理复杂的问题,适合处理需要多人协作的问题

对于复杂的事物,从宏观上,从整体上合理分析,需要用到面向对象的思维进行分析;具体到微观的操作上,又需要面向过程的思路去处理

面向对象编程(oop)的本质:以类的方式组织代码,以对象的方式组织(封装)数据

三大特性:封装、继承、多态

从认识论角度考虑是先有对象后有类。对象,是具体的事物。类,是抽象的,是对对象的抽象

从代码运行角度考虑是先有类后有对象。类是对象的模板

5.2 方法回顾和加深

方法的调用和参数传递见3.1.2和3.1.3

静态方法在类加载时随类一起加载,而非静态方法只有在类被实例化之后才存在

5.3 对象的创建分析

5.3.1 类与对象的关系

类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物

对象就是抽象概念的具体化实例

5.3.2 创建对象

使用new关键字创建对象

使用new关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中构造器的调用

类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有以下俩个特点:

  • 必须和类的名字相同

  • 必须没有返回类型,也不能写void

构造器必须要掌握

5.3.3 构造器(构造方法)

每个类即使没有显式的写出构造方法,Java也会给类写一个默认的构造方法

使用new关键字创建对象的本质是在调用构造器

构造器分为无参构造器和有参构造器,一旦定义了有参构造器,就必须显式的定义无参构造器

构造器的作用是初始化对象的属性

5.3.4 创建对象内存分析

参考链接:https://baijiahao.baidu.com/s?id=1637836912223474691&wfr=spider&for=pc

JVM内存结构:

  • 虚拟机栈:即平时提到的栈结构,我们将局部变量存储在栈结构中(对于main方法来说,对象引用就是一个局部变量,所以会存储在栈中)
  • 堆:我们将new出来的对象结构(比如,数组、对象)加载在堆空间中。对象的非static属性存储在堆空间
  • 方法区:存储类的加载信息、常量池、静态域
//实例
public class Application {
    public static void main(String[] args){
        Pet dog = new Pet();
        dog.name = "旺财";
        dog.age = 3;
        dog.shout();
    }
}

public class Pet {
    public String name;
    public int age;
    
    public void shout() {
        System.out.println("叫了一声");
    }
}

tip:JDK1.8方法区叫做元数据区,方法区(non-heap,非堆)不是在堆中(画图有误)

什么是引用类型:除了8大基本类型都可以叫做引用类型,对象是通过引用来操作的

new的实例对象存放在堆中,而我们操作的是存放在栈中的对象引用,其值就是实例对象在堆中的地址

5.4 面向对象三大特性

5.4.1 封装

该露的露,该藏的藏;我们的设计要追求“高内聚,低耦合”;高内聚就是类的内部数据操作细节由自己完成,不允许外部干涉;低耦合就是仅暴露少量的方法给外部使用

封装(数据的隐藏):通常,应禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问,这称为信息隐藏;记住:属性私有,get/set

public class Student {
    private String name;
    private int age;
    private char sex;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }
}

5.4.2 继承

参考链接:https://blog.csdn.net/hxhaaj/article/details/81174764

1.概述及特点:
  • 继承关系的俩个类,一个为子类(派生类),一个为父类(超类)。
  • 子类继承父类,使用关键字extends来表示;extends的意思是“扩展”,子类是父类的扩展;
  • 子类和父类之间,从意义上讲应该具有”is a”的关系
  • 继承是从已有类中派生出新的类,新的类能吸收已有类的属性和方法,并且能拓展新的属性和行为
  • 子类继承父类,表明子类是一种特殊的父类,子类拥有父类的属性和方法,并且子类可以拓展具有父类所没有的一些属性和方法
  • 子类即是不扩展父类,也能维持拥有父类的操作
  • Java类只有单继承,没有多继承,但支持多重继承(单继承,多实现,多重继承,即一个子类只能有一个父类,一个父类可以有多个子类,一个子类又可以被一个孙类继承)
  • 继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合等
  • object类是所有类的父类,在Java中即使一个类没有显式继承一个类,但也默认继承了Object类
  • final类不能被继承
2.继承的特性:
  • 成员变量和方法:
    • 子类拥有父类非 private 的属性、方法,但是可以通过父类提供的public的setter方法和getter方法进行间接地访问和操作private属性
    • 子类不能继承父类的静态方法和属性,因为静态的方法和属性是属于类本身的,但子类可以访问父类的静态方法和属性
    • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展
    • 子类可以用自己的方式实现父类的方法(方法重写),但子类和父类中同名的static变量和方法都是相互独立的,并不存在任何的重写的关系
    • 对于子类可以继承父类中的成员变量和成员方法,如果子类出现和父类同名的成员变量和成员方法时,父类成员变量会被隐藏,父类的成员方法会被覆盖。需要使用父类的成员变量和方法时,就需要使用super关键字来进行引用(隐藏是针对成员变量和静态方法,覆盖是针对普通方法
    • 父类严格上来说不能调用子类的方法和成员变量,但可以从以下几种方法进行考虑:
      • 在父类中直接new子类相关对象或者通过构造函数传入子类对象,然后调用其方法
      • 将子类相关方法声明为static,在父类中调用子类的static方法
      • 在父类中通过反射调用子类的相关方法
      • 通过注册监听,然后通过回调接口调用子类相关方法
      • 相关链接:https://www.jianshu.com/p/204e5d76ec11
    • 当创建一个子类对象时,即使子类定义了与父类中同名的实例变量,不仅会为该类的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存。 即依然会为父类中定义的、被隐藏的变量分配内存
    • 如果子类中的实例变量被私有了 ,其父类中的同名实例变量没有被私有,那么子类对象就无法直接调用该变量(子类中的),但可以通过先将对象变量强制向上转型为父类型,在通过该对象引用变量来访问那个实例变量,就会得到的是父类中的那个实例变量
  • 构造器:
    • 子类不能继承获得父类的构造方法,但是可以通过super关键字来访问父类构造方法
    • 在一个构造器中调用另一个重载构造器使用this调用完成,在子类构造器中调用父类构造器使用super调用来完成
    • super 和 this 的调用都必须是在第一句,否则会产生编译错误,this和super只能存在一个
    • 不能进行递归构造器调用,即多个构造器之间互相循环调用。
    • 如果父类有无参构造时,所有构造方法(包含任意有参构造)自动默认都会访问父类中的空参构造方法(自带super();)
      • 因为继承的目的是子类获取和使用父类的属性和行为,所以子类初始化之前,一定要先完成父类数据的初始化
      • 在Java中,每个类都会默认继承Object超类,所以每一个构造方法的第一条默认语句都是super()
    • 如果父类没有无参构造,反而有其他的有参构造方法时,子类继承父类后,子类必须显式的创建构造器,不论子类的构造器是否和父类构造器中参数类型是否一致,都必须在子类的构造器中显式的通过super关键字调用和父类构造器相应参数的构造方法。否则编译都通不过;也可以使用this先调用子类中的构造方法,再间接调用父类中的有参构造方法
    • 结论:当父类中没有无参构造器时,子类继承父类,子类中的构造器方法类型可以和父类中的构造器不同,但是必须每个构造器都显式的使用super关键字调用父类中的某个有参构造器,也可以使用this调用子类中的某个有参构造器,但这个有参构造器必须通过super访问父类中的有参构造器
  • super关键字:
    • super用于限定该对象调用它从父类继承得到的实例变量或方法
    • super和this相同,都不能出现在静态方法中,因为静态方法属于类的,调用静态方法的可能是个类,而不是对象,而super和this都是限定对象调用
    • super同样也可以在子类中调用父类中被子类隐藏和覆盖的同名实例变量和同名方法
    • 在构造器中使用super,则super会用于限定于该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。意思就是调用父类的构造器
    • super限定:super后紧跟一个点,super.namesuper.walk()
    • super调用:super后紧跟圆括号,super(无参或有参);
  • this关键字:
    • this关键字指向的是当前对象的引用
    • this.属性名称指的是访问类中的成员变量,用来区分成员变量和局部变量
    • this.方法名称用来访问本类的成员方法
    • this();访问本类的构造方法
    • this关键字把当前对象传递给其他方法method(this);
    • 当需要返回当前对象的引用时,常常使用return this;
  • 继承的代码执行顺序问题:
    • 构造器执行顺序

      • 当调用子类构造器实例化子类对象时,父类构造器总是在子类构造器之前执行
      • 创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果父类通过this调用了同类中的重载构造器,就会依次执行此父类的多个构造器
    • 静态域执行顺序

      • 当调用子类构造器实例化子类对象时,父类优先于子类进行加载到内存,所以会先执行父类中的静态域
      • 从该类所在继承树最顶层类开始加载,并执行其静态域,依次向下执行,最后执行本类。
      • 静态域优先于main方法,优先于构造器执行
3.方法重写

前提:需要有继承关系,子类重写父类的方法

方法重写规则:

  • 两同: 方法名形参列表相同

  • 两小:

    • 子类方法返回值类型应比父类方法返回值类型更小或相等

    • 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等

  • 一大:子类方法的访问权限应比父类方法访问权限更大或相等

方法重写Tips:

  • 父类中的私有方法不能被重写,该方法对于子类是隐藏的,因此其子类无法访问该方法,也无法重写
  • 父类静态方法,子类也必须通过静态方法进行覆盖,即静态只能覆盖静态
  • 子类重写父类方法时,最好声明得一模一样
  • 如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新的方法,所以该新方法不会受父类方法的任何限制
  • static方法(属于类,不属于实例)、final方法(常量)、private方法不能被重写

重写静态方法:方法的调用只和左边定义的引用相关

class A {
    private static void test() {
        System.out.println("A=>test");
    }
}
class B extends A {
    private static void test() {
        System.out.println("B=>test");
    }
}
public class Test {
    public static void main(String[] args) {
        B b = new B();
        b.test(); //输出B=>test
        A a = new B(); //向上转型,a引用指向B对象
        a.test(); //输出A=>test
    }
}

重写非静态方法:方法的调用需要看左边的引用指向new的哪个对象

class A {
    public void test() {
        System.out.println("A=>test");
    }
}
class B extends A {
    public void test() {
        System.out.println("B=>test");
    }
}
public class Test {
    public static void main(String[] args) {
        B b = new B();
        b.test(); //输出B=>test
        A a = new B(); //向上转型,a引用指向B对象
        a.test(); //输出B=>test
    }
}

方法重写和方法重载:

  • Override 和 Overload 的区别?Overload能改变返回值类型吗?

    • Override是重写,Overload是重载。重载可以改变返回值类型,它是方法名相同,参数列表不同,与返回值类型无关
  • 方法重写:子类中出现和父类中方法声明一模一样的方法。返回值类型相同(或者是子父类,多态),方法名和参数列表一模一样。主要发生在子类和父类的同名方法之间

  • 方法重载:本类中出现方法名相同参数列表不同的方法,和返回值类型无关,可以改变。主要发生同一类的多个同名方法之间

  • 子类对象调用方法时,先找子类本身的方法,再找父类中的方法

5.4.3 多态

1.相关规则及概念

多态即同一方法可以根据发送对象的不同而采用多种不同的行为方式。一个对象的实际类型是确定的,但可以指向对象的引用的类型有很多

多态存在的条件

  • 有继承关系

  • 子类重写父类方法父类引用指向子类对象

注意:多态是方法的多态,属性没有多态性。

public class Person {
    public void run() {
        System.out.println("Person");
    }
}
public class Student extends Person {
    public void run() {
        System.out.println("Student");
    }
    public void eat() {
        System.out.println("Student eat");
    }
}
public class Test {
    public static void main(String[] args) {
        //一个对象的实际类型是确定的
        //new Person()
        //new Student();
        
        //可以指向的引用类型就不确定了:自身类的引用指向自身,父类的引用指向子类
        
        Student s1 = new Student(); //能调用的都是自己扩展的和继承父类的方法
        Person s2 = new Student(); //父类引用指向子类对象,不能调用子类自身扩展的,只能调用子类重写的实例方法
        Object s3 = new Student();
        
        s1.run(); //输出Student。因为子类重写了父类的方法
        s2.run(); //当子类中没有任何方法时,会输出Person,因为子类继承了父类的run方法
                //当子类中重写了父类方法,会输出Student,因为Person类引用s2指向Student的对象,则会执行Student中的run方法
        s2.eat(); //这个写法是错误的,因为引用s2是作为Student对象的向上转型,不能调用子类Student自身扩展的方法
             //如果要调用eat()方法,需要将s2转换成Student引用类型(向下转型),即((Student)s2).eat(),此时才能调用eat()方法
    }
}
2.向上转型和向下转型:
  • 向上转型:子类对象转为父类,父类可以是接口;注意:向上转型不要强制转型。向上转型后父类的引用所指向的属性是父类的属性,如果子类重写了父类的方法,那么父类引用指向的或者调用的方法是子类的方法,这个叫动态绑定。向上转型后父类引用不能调用子类自身扩展的方法,如果调用不能编译通过

    public class Human {
        public void sleep() {
            System.out.println("Human sleep..");
        }
        public static void main(String[] args) {
            Human h = new Male();// 向上转型
            h.sleep();
            Male m = new Male();
            m.sleep();// h.speak();此方法不能编译,报错说Human类没有此方法
        }
    }
    class Male extends Human {
        @Override
        public void sleep() {
            System.out.println("Male sleep..");
        }
        public void speak() {
            System.out.println("I am Male");
        }
    }
    
  • 向下转型:父类对象转为子类,需要进行强制转换; 向下转型需要考虑安全性,如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现java.lang.ClassCastException错误。它可以使用instanceof来避免出错此类错误即能否向下转型,只有先经过向上转型的对象才能继续向下转型

  • 没有继承关系的两个类,互相转换,一定会失败

3.instanceof
boolean result = obj instanceof Class

  其中 obj 为一个对象,Class 表示一个类或者一个接口;当 obj 为 Class 的对象,或者是其直接或间接子类的对象,或者是其接口的实现类的对象,结果result 都返回 true,否则返回false。(说明Class只有大于等于obj时为true)

注意:

  • 编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定
  • 如果obj为null,那么则返回false
  • 编译状态中,Class可以是obj对象的父类,自身类,子类。在这三种情况下Java编译时不会报错。
  • 运行转态中,Class可以是obj对象的父类,自身类,不能是子类(class小于了obj)。在前两种情况下result的结果为true,最后一种为false。但是class为子类时编译不会报错。运行结果为false。 (大致就是判断表达式:class 变量=(class)object的引用 是否成立)

5.5 static和final关键字及代码块

5.5.1 static关键字详解

static关键字的使用:

  1. static:静态的

  2. static可以用来修饰:属性方法代码块内部类

  3. 使用static修饰属性:静态变量(或类变量)

    实例变量:我们创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性。当修改其中一个对象中的非静态属性时,不会导致其他对象中同样的属性值的修改

    静态变量:我们创建了类的多个对象,多个对象共享同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的

    static修饰属性的其他说明:

    ①静态变量随着类的加载而加载。可以通过类.静态变量的方式进行调用

    ②静态变量的加载要早于对象的创建

    ③由于类只会加载一次,则静态变量在内存中也只会存在一份:存在方法区的静态域中

类变量 实例变量
yes no
对象 yes yes

​ 4.使用static修饰方法:静态方法

​ 随着类的加载而加载,可以通过类.静态方法的方式进行调用

​ 静态方法中,只能调用静态的方法或属性;非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性

类方法 实例方法
yes no
对象 yes yes

​ 5.static注意点:在静态的方法内,不能使用this关键字、super关键字

//静态导入包
import static java.lang.Math.random;
import static java.lang.Math.PI;
//此时random和PI可以直接使用

5.5.2 final关键字详解

final关键字的使用:

  1. final:最终的

  2. final修饰一个类:此类不能被继承,比如String类,System类,StringBuffer类

  3. final修饰方法:表明该方法不能被重写,比如Object类中的getClass方法

  4. final修饰变量:此时的“变量”就是一个常量

  5. 1 final修饰属性:赋值的位置:显示初始化、代码块中初始化、构造器中初始化

  6. 2 final修饰局部变量:尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值

  7. static final修饰一个属性:表示它是一个全局常量

//面试题:排错
//1
public class Something {
    public int add0ne(final int x) {
        return ++X; //错误,因为返回X前对它进行了改变
        //return X + 1; //可以编译通过,因为X没有变化,只是返回的是X加1后的值
    }
}
//2
public class Something {
    public static void main(String[] args) {
        Other o = new Other();
        new Something().add0ne(o); 
    }
    public void add0ne(final 0ther o) {
        //o = new other();      //错误,因为传入的引用被final修饰,所以不能被改变
        o.i++;        //正确,引用o没变,只是改变其中的变量
    }
}
class Other {
    public int i;
}

5.5.3 代码块

代码块的作用:用来初始化类、对象

代码块如果有修饰的话,只能使用static

分类:静态代码块、非静态代码块

静态代码块

  • 内部可以有输出语句
  • 随着类的加载而执行,而且只执行一次
  • 作用:初始化类的信息
  • 如果类中声明了多个静态代码块,则按声明的先后顺序执行
  • 静态代码块优先于非静态代码块执行
  • 不能调用非静态属性和方法,只能调用静态属性和方法

非静态代码块

  • 内部可以有输出语句
  • 随着对象的创建而执行
  • 创建一个对象,就执行一次非静态代码块
  • 作用:可以在创建对象时,对对象的属性等进行初始化
  • 如果类中声明了多个非静态代码块,则按声明的先后顺序执行
  • 既可以调用静态属性和方法,又可以调用非静态属性和方法

类加载的执行顺序:静态代码块>匿名代码块>构造器

public static void main(String[] args) {
        new Leaf();                         
}
class Root {
    static {
        System.out.println("Root的静态代码块");
    }
    {
        System.out.println("Root的普通代码块");
    }
    public Root() {
        super();
        System.out.println("Root的无参构造器");
    }
}
class Mid extends Root{
    static {
        System.out.println("Mid的静态代码块");
    }
    {
        System.out.println("Mid的普通代码块");
    }
    public Mid() {
        super();
        System.out.println("Mid的无参构造器");
    }
}
class Leaf extends Mid {
    static {
        System.out.println("Leaf的静态代码块");
    }
    {
        System.out.println("Leaf的普通代码块");
    }
    public Leaf() {
        super();
        System.out.println("Leaf的无参构造器");
    }
}
/*输出为:Root的静态代码块
         Mid的静态代码块
         Leaf的静态代码块
         Root的普通代码块
            Root的无参构造器
         Mid的普通代码块
         Mid的无参构造器
         Leaf的普通代码块
         Leaf的无参构造器*/

5.6 抽象类和接口

5.6.1 抽象类

abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法;如果修饰类,那么该类就是抽象类

抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类

抽象类,不能使用new关键字来创建对象,它是用来让子类继承的(所以抽象类一定有构造器)

抽象方法,只有方法的声明,没有方法的实现,它是用来让子类实现

子类继承抽象类,那么就必须要实现抽象类没有实现的抽象方法,否则该子类也要声明为抽象类

1.抽象类的匿名子类对象
abstract class Person {
    public abstract void eat();
}
class Worker extends Person {
    @Override
    public void eat() {
    }
}

public static void main(String[] args) {
        method(new Worker());//1.非匿名类的匿名对象
        Worker worker = new Worker();//2.非匿名类的非匿名对象
        //3.创建抽象类匿名子类的非匿名对象
        Person person = new Person() {
            @Override
            public void eat() {
            }
        };
        method1(person);
        //4.抽象类匿名子类的匿名对象
        method1(new Person(){
            @Override
            public void eat() {
            }
        });
    }
    public static void method(Worker worker){
    }
    public static void method1(Person person){
    }
2.模板方法设计模式

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式

解决的问题:

  • 当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现
  • 换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式
public static void main(String[] args) {
    SubTemplate t = new SubTemplate();
    t.spendTime();
}
abstract class Templatet {
    //计算某段代码执行所需要花费的时间
    public void spendTime(){
        long start = System. currentTimeMillis();
        this.code();//不确定的部分、易变的部分
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:"+ (end - start));
    }
    public abstract void code();
}
class SubTemplate extends Template{
    @Override
    public void code() {
        //要执行的代码
    }
}

5.6.2 接口

普通类:只有具体实现

抽象类:具体实现和规范(抽象方法)都有

接口:只有规范,没有构造器,意味着不能被实例化

接口就是规范,定义的是一组规则。接口的本质是契约,就像我们人类的法律一样。制定好后大家都遵守

声明类的关键字是class,声明接口的关键字是interface

OO的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如c++、java、c#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象

Tips:

  • 接口中的方法定义都是public abstract,书写时可以省略
  • 接口中的属性都是常量,public static final修饰的,书写时可以省略
  • 实现接口(implements)的类必须重写接口中的所有方法,否则这个实现类还是一个抽象类
  • 接口的实现允许多实现(弥补类不能多继承的局限性,格式class A extends B implements C,D)
  • 接口之间可以继承,而且可以多继承
  • 接口的具体使用,体现出多态性
  • JDK7及以前接口中只能定义全局常量和抽象方法;JDK8及之后还可以定义静态方法和默认方法
1.Java8中接口的新特性
//首先定义一个接口
public interface CompareA {
    //静态方法
    public static void method1() {
        System.out.println("CompareA:method1");
    }
    //默认方法
    public default void method2() {
        System.out.println("CompareA:method2");
    }
    public default void method3() {
        System.out.println("CompareA:method3");
    }
}
  • 1.接口中的静态方法只能通过接口名.静态方法进行调用

  • 2.接口中的默认方法可以通过实现类的对象进行调用,如果实现类重写了这个默认方法,那么调用的还是重写后的方法

    public class SubClassTest {
        public static void main(String[] args) {
            SubClass s = new SubClass();
            CompareA.method1(); //输出=>CompareA:method1,1.接口中的静态方法只能通过`接口名.静态方法`进行调用
            s.method2();//输出=>CompareA:method2,2.接口中的默认方法可以通过实现类的对象进行调用
            s.method3();//输出=>SubClass:method3,2.如果实现类重写了这个默认方法,那么调用的还是重写后的方法
        }
    }
    class SubClass implements CompareA{
        @Override
        public void method3() {
            System.out.println("SubClass:method3");
        }
    }
    
  • 3.如果在子类(或实现类)继承的父类实现的接口中声明了同名同参数的默认方法,那么子类在没有重写此方法的情况下默认调用的是父类中的同名同参数的方法—>类优先原则

    public class SubClassTest {
        public static void main(String[] args) {
            SubClass s = new SubClass();
            s.method2();//输出=>SuperClass:method2, 3.类优先原则
        }
    }
    class SubClass extends SuperClass implements CompareA {
    }
    class SuperClass {
        public void method2() {
            System.out.println("SuperClass:method2");
        }
    }
    
  • 4.如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,出现报错 —>接口冲突。这就需要我们必须在实现类中重写此方法

    public class SubClassTest {
        public static void main(String[] args) {
            SubClass s = new SubClass();
            s.method2();//编译出错   4.接口冲突,需要我们必须在实现类中重写此方法
        }
    }
    public interface CompareB {
        //默认方法
        public default void method2() {
            System.out.println("CompareB:method2");
        }
    }
    class SubClass implements CompareA,CompareB {
    }
    
  • 5.在子类 (或实现类)的方法中调用父类中被重写的方法使用super.方法名,调用接口中被重写的方法使用接口名.super.方法名

    class SubClass extends SuperClass implements CompareA {
        public void method2() {
            System.out.println("SubClass:method2");
        }
        public void MyMethod() {
            method2(); //在自己的方法中调用自己重写的方法
            super.method2();//在自己的方法中调用父类中被重写的方法使用super.方法名
            CompareA.super.method2();//在自己的方法中调用接口中被重写的方法使用接口名.super.方法名
        }
    }
    class SuperClass {
        public void method2() {
            System.out.println("SuperClass:method2");
        }
    }
    
2.接口应用实例:代理模式

代理模式是Java开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问

应用场景:

  • 安全代理:屏蔽对真实角色的直接访问
  • 远程代理:通过代理类处理远程方法调用( RMI)
  • 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象

分类:

  • 静态代理(静态定义代理类)
  • 动态代理(动态生成代理类);JDK自带的动态代理,需要反射等知识
public static void main(String[] args){
    Server server = new Server();
    ProxyServer proxyserver = new ProxyServer(server);
    proxyServer.browse();
}
interface NetWork {
    public void browse();
}
//被代理类
class Server implements NetWork{
    @Override
    public void browse() {
        System.out.println("真实的服务器访问网络");
    }
}
//代理类
class ProxyServer implements NetWork{
    private NetWork work;
    public ProxyServer(NetWork work){
        this.work = work;
    }
    public void check(){
        System.out.println("联网之前的检查工作");
    }
       @Override
    public void browse(){
        check();
        work.browse();
    }
}
3.接口应用实例:工厂模式

工厂模式:实现了创建者与调用者的分离,即将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的

分类:

  • 简单工厂模式:用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
    • 缺点:对于增加新产品,不修改代码的话,是无法扩展的。违反了开闭原则(对扩展开放,对修改封闭)
  • 工厂方法模式:用来生产同一等级结构中的固定产品。(支持增加任意产品)
  • 抽象工厂模式:用来生产不同产品族全部产品。(对于增加新的产品,无能为力;支持增加产品族)

5.7 内部类

内部类就是在一个类的内部在定义一个类,比如,A类中定义一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类

成员内部类(静态、非静态) VS 局部内部类(方法内、代码块内、构造器内)

参考链接:https://www.jianshu.com/p/79f6d2aca1d8

5.7.1 成员内部类

一方面,作为外部类的成员:

  • 调用外部类的结构

  • 可以被static修饰

  • 可以被4种不同的权限修饰

另一方面,作为一个类:

  • 类内可以定义属性、方法、构造器等

  • 可以被final修饰,表示此类不能被继承。言外之意,不使用final,就可以被继承

  • 可以被abstract修饰

    //获取内部类对象实例
    public class Test {
        public static void main(String[] args) {    
            Person ricky = new Person();
            //获取成员内部类对象实例,方式1:new 外部类.new 内部类
            Person.Heart heart = new Person().new Heart();
            System.out.println(heart.beat());
            //获取成员内部类对象实例,方式1:外部类对象.new 内部类
            heart = ricky.new Heart();
            //获取成员内部类对象实例,方式2:外部类对象.获取方法
            Person.Heart heart2 = ricky.getHeart();
            System.out.println(heart2.beat());
        }
    }
    //成员内部类的使用
    public class Person {
        String name = "大大";
        //获取内部类对象方法
        public Heart getHeart(){
            //外部类访问内部类信息,需要通过内部类实例,无法直接访问name
            new Heart().name = "明明";
            return new Heart();
        }
        public void eat(){
            System.out.println("人会吃东西");
        }
        //访问修饰符可以任意,设置为private则只能在此外部类中创建实例
        private class Heart {
            String name = "小小";
            public String beat(){
                //同名属性这里优先访问的是内部类中的name
                String str = name + "的心脏在跳动";
                //访问外部类中的同名属性
                str = Person.this.name + "的心脏在跳动";
                eat();//直接调用eat()优先访问的是内部类中定义的
                Person.this.eat(); //调用外部类的同名方法
                return str;
            }
            //同名方法
            public void eat(){
                System.out.println("吃东西");
            }
        }
    }
    
    • 成员内部类可以无条件访问外部类的属性和方法

    • 外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法

    • 成员内部类里面不能含静态属性或方法

    • 如果成员内部类的属性或者方法与外部类的同名,将导致外部类的这些属性与方法在内部类被隐藏

    • 访问同名属性和方法时优先访问内部类中定义的;访问外部类中的同名属性和方法可按照该格式调用外部类.this.属性/方法

    • 成员内部类访问权限:成员内部类前可加上四种访问修饰符。

      private:仅外部类可访问
      protected:同包下或继承类可访问
      default:同包下可访问
      public:所有类可访问

    • 内部类编译后.class文件的名称:外部类$内部类.class

5.7.2 静态内部类

//定义格式
class U {
    static class I {       
    }
}
//使用实例
public class Person {
    String name = "大大";
    public static int age = 22;
    //获取内部类对象方法
    public Heart getHeart(){
        return new Heart();
    }
    public void eat(){
        System.out.println("人会吃东西");
    }
    //静态内部类
    public static class Heart {
        String name = "小小";
        public static int age = 12;
        public String beat(){
            //静态内部类中,只能直接访问外部类的静态成员,如果需要调用非静态成员,可以通过对象实例
            //直接访问eat()会报错
            new Person().eat();
            //内部类中的成员方法可以直接访问内部类中的非静态成员和静态成员
            String str = name + age + "岁的心脏在跳动";
            //访问外部类中的非静态成员和同名静态成员
            return new Person().name + Person.age + "的心脏在跳动";
        }
    }
    //测试
    public static void main(String[] args) {
        //获取静态内部类对象实例
        Person.Heart heart = new Person.Heart();
        System.out.println(heart.beat());
        //可以通过外部类.内部类.静态成员的方式,访问内部类中的静态成员
        Person.Heart.age = 15;
    }
}
  • 静态内部类和成员内部类相比多了一个static修饰符,它与类的静态成员变量一样,不依赖于外部类对象
  • 获取静态内部类对象实例:new 外部类名.静态类()
  • 静态内部类中,只能直接访问外部类的静态成员,如果需要调用非静态成员,可以通过外部类对象实例访问
  • 可以通过外部类.内部类.静态成员的方式,访问内部类中的静态成员
  • 当内部类中静态成员与外部类静态成员同名时,默认直接调用内部类中的成员,可以通过 外部类.成员 的方式访问外部类的同名静态成员,而对于外部类中的非静态成员,不管同不同名,都需要通过外部类实例访问
  • 静态内部类也有它的特殊性,因为外部类加载时只会加载静态域,所以静态内部类中不能使用外部类的非静态变量与方法

5.7.3 局部内部类

//定义格式
class K{
    public void say(){
        class J{           
        }
    }
}
//使用实例
public class Person {
    String name = "大大";
    public static int age = 22;

    public Object getHeart(){
        int num = 10;
        //方法内部类:不能使用任何访问修饰符,不能使用static修饰
        class Heart {
            String name = "小小";
            //类成员可以使用访问修饰符、final、abstract,但不能使用static修饰
            public final int age = 12;
            
            public String beat(){
                System.out.println(num);//内部类想要使用所在方法的局部变量时,该变量必须被声明为final(JDK8及之后不用显示声明)
                new Person().eat(); //访问外部类的非静态方法,通过实例对象
                return name + Person.age + "的心脏在跳动"; //Person.age访问外部类的静态变量
            }
        }
         return new Heart().beat();//调用内部类的非静态方法
    }
    public void eat(){
        System.out.println("人会吃东西");
    }
    //测试
    public static void main(String[] args) {
        Person ricky = new Person();
        //调用包含方法内部类的方法
        System.out.println(ricky.getHeart());   
    }
}
  • 局部内部类存在于方法中,它与成员内部类的区别在于局部内部类的访问权限和作用范围仅限于方法或作用域内
  • 方法内部类不能使用任何访问修饰符,不能使用static修饰
  • 类中不能包含静态成员,但可以包含finalabstract修饰的成员

5.7.4 匿名内部类

//完成一个不同的人进行阅读的操作
//定义抽象父类
public abstract class Person {
    //阅读方法
    public abstract void read();
}
//定义子类
public class Man extends Person{
    @Override
    public void read() {
        System.out.println("男生喜欢看科幻类书籍");       
    }
}
public class Women extends Person{
    @Override
    public void read() {
        System.out.println("女生喜欢读言情小说");        
    }
}
//使用传统的多态方式实现以及匿名内部类实现
public class Test {
    //需求:根据传入的不同的人的类型,调用对应的read方法
    public void getRead(Person person){
        person.read();
    }
    public static void main(String[] args) {
        //方案一:利用多态调用对应子类的实现
        Test test = new Test();
        Man one=new Man();
        Woman two=new Woman();
        test.getRead(one);
        test.getRead(two);  
        //方案二:不定义任何子类,使用匿名内部类完成具体的read方法实现
        test.getRead(new Person(){
            @Override
            public void read() {
                System.out.println("男生喜欢看科幻类书籍");
            }
        });
        test.getRead(new Person(){
            @Override
            public void read() {
                System.out.println("女生喜欢读言情小说");
            }
        });
    }
}
//匿名内部类在合适的场景下对于内存的损耗和对系统的性能影响就会相对较小,弊端就是只能使用一次,无法重复使用

5.7.5 适用场景

  • 只用到类的一个实例

  • 类在定义后马上用到

  • 给类命名并不会导致代码更容易被理解

//1.继承式的匿名内部类
public class Demo {
    public static void main(String[] args) {
        //继承式的匿名内部类(相当于定义了一个匿名的Thread子类,目的是重写其方法)
        Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i <= 5; i++) {
                    System.out.println(i + " ");
                }
            }
        };
        thread.start();
    }
}
//2.接口式的匿名内部类
public class Demo {
    public static void main(String[] args) {
        //接口式的匿名内部类(相当于创建了一个实现了接口的匿名类)
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 5; i++) {
                    System.out.println(i + " ");
                }
            }
        };
        Thread thread = new Thread(r);
        thread.start();
    }
}
//3.参数式的匿名内部类
public class Demo {
    public static void main(String[] args) {
        //参数式的匿名内部类
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 5; i++) {
                    System.out.println(i + " ");
                }
            }
        });
        thread.start();
    }
}

结论:由上面三个例子可以看出,匿名内部类可以继承一个具体的父类,也可以实现某个接口。只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现

注意:

  • 匿名内部类没有类型名称、实例对象名称
  • 编译后的文件命名:外部类$数字.class
  • 无法使用访问修饰符、也无法使用abstractstatic修饰
  • 无法编写构造方法,也是唯一没有构造方法的内部类,可以添加构造代码块,通过代码块完成匿名内部类的初始化
  • 不能出现静态成员,不能出现抽象方法
  • 匿名内部类可以实现接口也可以继承父类,但是不可兼得
  • 匿名内部类和局部内部类只能访问外部类的final变量

内部类的好处:

  • 完善了Java多继承机制,由于每一个内部类都可以独立的继承接口或类,所以无论外部类是否继承或实现了某个类或接口,对于内部类没有影响
  • 方便写事件驱动程序

5.8 Object类浅析

Object类是所有Java类的根父类

如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类

Object类中的功能(属性、方法)具有通用性

Object类只声明了一个空参构造器

public class Person {...}等价于public class Person extends Object {...}

//例: method(Object obj){...}可以接收任何类作为其参数
//Person o=new Person();
//method(o);

5.8.1 Object类中的方法

  • Object类没有属性
  • clone():克隆一个对象并返回
  • equals():比较两个对象是否相等
  • finalize():JVM进行垃圾回收前要执行的方法,通常情况都是垃圾回收器自动调用,程序员可以通过System.gc()或者Runtime.getRuntime().gc()来通知系统进行垃圾回收,但是系统是否进行依然不确定(垃圾回收机制只回收JVM中堆内存中的对象空间)(面试题:final、finally、finalize的区别)
  • getClass():获取当前对象的所属类
  • hashcode():返回当前对象的哈希值
  • toString():对象打印时调用,在Object中是返回当前对象的类名和引用地址,如果被其他类(如String类、Date类、File类、包装类等)重写就会执行重写后的方法
  • notify()、notifyAll()、wait():线程相关方法

5.8.2 ==和equals()方法的区别

  1. 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较,对于引用类型就是比较内存地址

  2. equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是;我们可以看到string等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点(比如String类、Date类、File类、包装类等都重写了Object类的equals方法,重写后,不是比较的两个引用的地址是否相同,而是比较两个对象的”实体内容”是否相同)

  3. 具体要看自定义类里有没有重写Object的equals方法来判断

  4. 通常情况下,重写equals方法,会比较类中的相应属性是否都相等

  5. Object类中equals()方法和==的作用一样,都是比较两个对象的地址值是否相同

    //Object类中的equals方法,比较引用地址
    public boolean equals(Object obj) {
        return (this == obj);
    }
    //String类中的equals方法,首先比较引用是否相同,如不同再比较两个对象的各个属性值是否相同
    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }
    

5.8.3 toString()方法

toString()方法在Object类中定义,其返回值是String类型,返回类名和它的引用地址

在进行String与其它类型数据的连接操作时,自动调用toString()方法(与基本类型的连接可参考1.3.3)

Date now=new Date();
system.out.printIn("now="+now);//相当于system.out.printIn("now=+now.toString());输出=>now=当前时间

可以根据需要在用户自定义类型中**重写toString()**方法如String 类重写了toString()方法,返回字符串的值。

s1="hello";
system.out.printIn(s1);//相当于System.out.printIn(s1.toString());输出=>hello

基本类型数据转换为String类型时,调用了对应包装类的toString()方法

int a=10;
system.out.printIn("a="+a);

5.9 包装类

5.9.1 装箱和拆箱

int num = 10;
//装箱
Integer i = new Integer(num);
//拆箱
int num1 = Integer.intValue(i);

//其他其中基本类型的装箱和拆箱操作也如上

5.9.2 自动拆箱和自动装箱(JDK5版本之后的新特性)

int i = 0;
//自动装箱
Integer num = i;
//自动拆箱
int m = num;

//基本数据类型的自动装箱和拆箱只适用于变量的值在相应包装类中缓冲区范围内,超出范围则需要通过5.8.1的方式来进行拆箱和装箱
//比如,int类型对应的Integer包装类中缓冲区的数组范围为-128~127,如果一个int i= 130需要进行装箱,则需要new Integer(i)来进行装箱

//其他几种基本数据类型也能进行装箱拆箱

通过将基本数据类型进行装箱操作后可以调用封装类中的方法和变量,常量

将一个基本数据类型的变量进行封装后可以判断这个变量的值是否为null

5.9.3 基本数据类型和String类型的相互转换

//包装类--->String
//方法一:连接运算
int num = 10;
String str1 = num + "";
//方法二:调用String的ValueOf方法
float f = 12.3f;
String str2 = String.ValueOf(f);
//String--->基本数据类型、包装类
String str3 = "123";
int num1 = Integer.parseInt(Str3);
//错误情况:int num1 = (int)str3;或者Integer i = (Integer)str3;

5.10 单例设计模式

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式

设计模式分类:

  • 创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
  • 结构型模式,共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
  • 行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new在类的外部创建类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的(简单来说,单例模式(Singleton模式)指的是一个类,在一个JVM里,只有一个实例存在;且构造方法私有化

单例模式优点:由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决

5.10.1 单例模式饿汉式

饿汉式是无论如何都会创建一个实例对象

class Bank {
    //1.内部创建类的对象
    //2.由于静态方法只能访问静态属性,所以这个对象属性必须为static
    priavte static Bank instance = new Bank();
    //3.构造方法私有化
    private Bank() {
    }
    //4.提供公共的静态方法返回类的对象
    public static Bank getInstance() {
        return instance;
    }
}

5.10.2 单例模式懒汉式

懒汉式只有在调用getInstance的时候,才会创建实例

class Bank {
    //1.准备一个属性,用于指向一个实例化对象,但是暂时指向null
    //2.由于静态方法只能访问静态属性,所以这个对象属性必须为static
    priavte static Bank instance;
    //3.构造方法私有化
    private Bank() {
    }
    //4.提供公共的静态方法返回类的对象
    public static Bank getInstance() {
        if(null == instance) { //第一次访问的时候,发现instance没有指向任何对象,这时实例化一个对象
            instance = new Bank();
        }
        return instance; //返回instance指向的实例对象
    }
}

5.10.3 饿汉式和懒汉式的区分

饿汉式:

  • 好处:线程安全,只能创建出一个实例对象
  • 坏处:对象加载时间过长

懒汉式:

  • 好处:延迟对象的创建
  • 坏处:线程不安全,有创建出多个实例对象的可能—->到多线程时再修改

5.11 MVC设计模式

MVC是常用的设计模式之一,将整个程序分为三个层次:视图模型层控制器层,与数据模型层。这种将程序输入输出数据处理,以及数据的展示分离开来的设计模式使程序结构变的灵活而且清晰,同时也描述了程序各个对象间的通信方式,降低了程序的耦合性

模型层 model主要处理数据

数据对象封装 model.bean/domain

数据库操作类 model.dao

数据库 model.db

视图层 view显示数据

相关工具类 view.utils
自定义view view.ui

控制层 controller处理业务逻辑

应用界面相关 controller.activity

存放fragment controller.fragment

显示列表的适配器 controller.adapter

服务相关的 controller.service

抽取的基类 controller.base

六、Java异常和错误

6.1 什么是异常?

软件程序在运行过程中,非常可能遇到刚刚提到的这些异常问题,我们叫异常,英文是:Exception,意思是例外。这些,例外情况,或者叫异常,怎么让我们写的程序做出合理的处理。而不至于程序崩溃

异常指程序运行中出现的不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等;异常发生在程序运行期间,它影响了正常的程序执行流程。

6.1.1 异常分类

检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略

运行时异常:运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在
编译时被忽略

错误(ERROR):错误不是异常, 而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢
出时,一个错误就发生了,它们在编译也检查不到的

6.1.2 异常体系结构

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类

在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception

6.2.3 Error

Error类对象由Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关

Java虚拟机运行错误(Virtual MachineError) , 当JVM不再有继续执行操作所需的内存资源
时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM) - -般会选择线程终

还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError) 、链接错误
(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且
绝大多数是程序运行时不允许出现的状况

6.2.4 Exception

在Exception分支中有一个重要的子类RuntimeException (运行时异常):

  • ArrayIndexOutOfBoundsException (数组下标越界)

  • NullPointerException (空指针异常)

  • ArithmeticException (算术异常)

  • MissingResourceException (丢失资源)

  • ClassNotFoundException (找不到类)等异常,这些异常是不检查异常,程序中可以选
    择捕获处理,也可以不处理

这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生

Error和Exception的区别: Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程; Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常

6.2 异常处理机制

分为:抛出异常捕获异常

子类重写的方法抛出的异常不大于父类被重写的方法抛出的异常

异常处理的5个关键字:trycatchfinallythrowthrows

//捕获异常
try {
    //将要执行,被捕获异常的语句,放在try语句块中
} catch(Exception e) {
    //如果上方语句出现相应异常,则执行catch语句块中的语句
} finally {
    //无论try中的代码语句是否有被捕获到异常,然后转到catch块进行处理,finally中的语句都会执行
    //finally语句块是否使用为可选,可以使用,也可以不使用
}
//一个try代码块后面可跟随多个catch代码块,这种情况叫做多重捕获
//若需要捕获多个异常,需要从小到大
//抛出异常
//如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。
//也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的
public class Test {
    //假设方法中处理不了这个异常,则在方法上进行抛出
    //一个方法可以抛出多个异常,中间用逗号隔开
    public void test() throws Exception {
        throw new Exception(); //主动抛出异常,一般在方法中使用
    }
}

开发中如何选择使用try-catch- finally还是使用throws?

①如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法中有异常,必须使用try- catch- finally方式处理

②执行的方法a中,先后又调用了另外的几个方法,这几个方法是递进关系执行的。建议这几个方法使用throws的方式进行处理。而执行的方法a可以考虑使用try- catch-finally方式进行处理

6.3 自定义异常

使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常。用户自定义异常类,只需继承Exception类即可

编写自己的异常时,需要注意几点:

  • 所有异常都必须是 Throwable 的子类
  • 如果希望写一个检查性异常类,则需要继承 Exception 类
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类

在程序中使用自定义异常类,大体可分为以下几个步骤:

  1. 创建自定义异常类
  2. 在方法中通过throw关键字抛出异常对象。
  3. 如果在当前抛出异常的方法中处理异常,可以使用try-catch语句捕获并处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作
  4. 在出现异常方法的调用者中捕获并处理异常
//自定义异常
public class MyException extends Exception{
    //传递数字
    private int number;

    public MyException(int a) {
        this.number = a;
    }
    //toString:异常的打印信息
    @Override
    public String toString() {
        return "MyException{" + "number=" + number + '}';
    }
}
//使用自定义异常
public class test06 {
    public static void main(String[] args) {
        try {
            addNumber(11); //因为传给addNumbers方法的值为11,所以 报错输出MyException=>MyException{number=11}
        } catch (Exception e) {
            System.out.println("MyException=>" + e);
        }
    }

    private static void addNumber(int a) throws MyException{
        if (a > 10) {
            throw new MyException(a);
        }
        System.out.println(a);
    }
}
//为什么第21行输出e会自动调用toString方法?
//因为在MyException中重写了父类中的toString方法,System.out.println则会调用子类重写的这个toString方法,否则他就会调用父类中的toS 方法

七、Java多线程

7.1 基本概念:程序、进程、线程

程序(program)为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象

进程(process)程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期

  • 如:运行中的QQ,运行中的MP3播放器
  • 程序是静态的,进程是动态的
  • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • **线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)**,线程切换的开销小
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患

JVM结构:

7.1.1 CPU、并行与并发的理解

单核CPU和多核CPU的理解

  • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来
  • 如果是多核的话,才能更好的发挥多线程的效率(现在的服务器都是多核的)
  • 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程

并行与并发

  • 并行:多个CPU同时执行多个任务(多个人同时做不同的事(多对多),如,篮球场上的人(多个CPU)同时在做不同的事(多个任务))
  • 并发:一个CPU(采用时间片)同时执行多个任务(一个人同时做不同的事(一对多),比如,篮球场(一个CPU)上每个人做的事(多个任务))

7.1.2 多线程的优点

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

7.1.3 什么时候需要多线程

  1. 程序需要同时执行两个或多个任务
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
  3. 需要一些后台运行的程序

7.1.4 线程的分类

Java中的线程分为两类:一种是守护线程,一种是用户线程

  • 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
  • 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程
  • Java垃圾回收就是一个典型的守护线程
  • 若JVM中都是守护线程,当前JVM将退出

7.2 线程的创建和使用

7.2.1 线程的调度

调度策略:

  • 时间片:每个线程占用CPU一段时间

  • 抢占式:高优先级的线程抢占CPU

Java的调度方法

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

线程的优先级等级

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5

涉及的方法

  • getPriority():返回线程优先值
  • setPriority(int newPriority):改变线程的优先级

说明

  • 线程创建时继承父线程的优先级
  • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程通信:wait()、notify()、notifyAll()此三个方法定义在Object类中

7.2.2 多线程的创建

1.方法一:继承Thread类
  1. 创建一个继承于Thread类的子类
  2. 重写Thread类的run() —>将此线程要执行的操作声明在run()中
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start() —>①启动当前线程 ②调用当前线程的run()方法 ③每个线程只能start一次
  5. 不能直接调用run()方法,否则相当于普通方法的调用在main线程中执行,不会有线程之间的交互进行
public class ThreadTest01  {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //3.在main线程中创建一个新的线程
        myThread.start(); //4.调用start()方法,开始执行这个线程的操作,此时会从主线程另外分出一个线程执行命令
        System.out.println("**************"); //下面是主线程需要执行的操作,从输出结果可以看出两个线程的交互执行
        for (int i = 0; i < 100; i++) { //遍历100以内的奇数
            if(i % 2 != 0) System.out.println(i + "main()");
        }
    }
}
class MyThread extends Thread { //1.继承Thread类
    @Override
    public void run() { //2.重写run()方法
        for (int i = 0; i < 100; i++) { //遍历100以内的偶数
            if(i % 2 == 0) System.out.println(i);
        }
    }
}
//创建两个分线程,一个打印100以内的偶数,一个打印100以内的奇数
public static void main(String[] args) {
    //方法一:创建两个线程子类,再分别创建一个对象,在各自的run方法中重写不同操作
    //方法二:创建两个线程的匿名子类对象,如下:
    new Thread(){
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if(i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }.start();
    new Thread(){
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if(i % 2 != 0) System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }.start();
}
2.方法二:实现Runnable接口
  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法: run( )
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()
class  MyThread implements Runnable { //1.创建一个实现了Runnable接口的类
    @Override
    public void run() {  //2.实现类去实现Runnable中的抽象方法: run()
        for (int i = 0; i < 100; i++) {
            if(i%2==0) {
                System.out.println(i);
            }
        }
    }
}
public class ThreadTest03 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();  //3.创建实现类的对象
        Thread t1 = new Thread(myThread);  //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        t1.start();  //5.通过Thread类的对象调用start()
    }
}
//多窗口抢票实例
//目前存在线程安全问题,即第100张票可能被多个窗口同时卖出,等到7.4节得以解决
class window extends Thread {
    private static int stick = 100; //必须加上static才能让三个线程共享100张票,否则会出现每个线程都有100张票的情况
    @Override                   //因为这种线程创建的方式是一个线程为一个对象,而实现接口的方式是一个对象创建三个线程
    public void run() {
        while (true) {
            if(stick > 0) {
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + stick);
                stick--;
            } else {
                break;
            }
        }
    }
}
class window implements Runnable {
    private int stick = 100; //不用加上static也能让三个线程共享100张票,因为这三个线程都是由同一个对象创建的
    @Override
    public void run() {
        while (true) {
            if(stick > 0) {
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + stick);
                stick--;
            } else {
                break;
            }
        }
    }
}
public class ThreadTest02 {
    public static void main(String[] args) {
        //继承Thread类的方式实现
        window t1 = new window();
        window t2 = new window();
        window t3 = new window();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
        //实现Runnable接口的方式
        window window = new window();
        Thread t1 = new Thread(window);
        Thread t2 = new Thread(window);
        Thread t3 = new Thread(window);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
3.两种线程创建方式的比较

开发中优先选择:实现Runnable接口的方式

原因

  1. 实现的方式没有类的单继承性的局限性
  2. 实现的方式更适合来处理多个线程有共享数据的情况

联系: public class Thread implements Runnable

相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run( )中

7.2.3 Thread类中的有关方法(使用)

  • void start():启动线程,并执行对象的run()方法
  • run():线程在被调度时执行的操作
  • String getName():返回线程的名称
  • void setName(String name):设置该线程名称
  • static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
  • static void yield():线程让步
    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    • 若队列中没有同优先级的线程,忽略此方法
  • join():当某个程序执行流中调用其他线程的 join()方法时,调用线程将被阻塞,直到join()方法加入的join 线程执行完为止
    • 低优先级的线程也可以获得执行
  • static void sleep(long millis):(指定时间:毫秒)
    • 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
    • 抛出InterruptedException异常
  • stop():强制线程生命期结束,不推荐使用,已过时
  • boolean isAlive():返回boolean,判断线程是否还活着

7.3 线程的生命周期

JDK中用Thread.State类定义了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是还没分配到CPU资源
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

7.4 线程的同步

7.4.1 线程的安全问题

问题提出:

  • 多个线程执行的不确定性引起执行结果的不稳定
  • 多个线程对账本的共享,会造成操作的不完整性,会破坏数据

如,7.2.2中多窗口卖票实例若run()中加了sleep()的可能会出现:

7.4.2 线程安全问题的解决办法(同步和Lock)

7.2.2实例出现的问题:卖票过程中,出现了重票、错票–>出现了线程的安全问题

问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票

如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,ticket也不能被改变

在Java中,我们通过同步机制和Lock来解决线程安全问题

利用同步机制解决线程安全问题的优缺点:

  1. 同步的方式,解决了线程的安全问题 —好处
  2. 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低 —缺点
1.方式一:同步代码块
synchronized(同步监视器){
    //需要被同步的代码
}
//1.操作共享数据的代码,即为需要同步的代码  --->不能包含代码多了,也不能包含少了
//2.共享数据:多个线程共同操作的变量,比如ticket就是共享数据
//3.同步监视器,俗称:锁;任何一个类的对象,都可以充当锁
//    要求:多个线程必须要公用同一个锁

//补充:在实现Runnable接口实现创建多线程的方式中,我们可以考虑使用this充当同步监视器
//        在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类作为同步监视器

①解决实现Runnable的线程安全问题

class window implements Runnable {
    private int ticket = 100;
    Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized(obj){  //synchronized(this)这种方式在实现Runnable的方式中也可以,因为这个this代表的是
                 if(ticket > 0) {   //window的对象window,此时仍然是三个线程公用一把锁
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }   
            }
        }
    }
}

②解决继承Thread的线程安全问题

class window extends Thread {
    private static int ticket = 100; 
    private static Object obj = new Object();//必须用static修饰,因为继承Thread的方式需要每创建一个线程就声明一个
    @Override                   //对象,如果不加static就会让每个对象都有一把锁,就违背了多个线程必须共用一把锁的要求
    public void run() {
        while (true) {
            synchronized(obj) {  //synchronized(this)这种方式在继承Thread的方式中就不可以,因为这个this代表的是当
                if(ticket > 0) {  //前线程的对象t1、t2、t3,即一个线程一把锁
//但我们可使用synchronized(window.class)作为锁的方式来实现,因为window.class也是一个对象,此时三个线程也是公用一把锁
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
2.方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的

①解决实现Runnable接口的线程安全问题

class  MyThread implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();
            if(ticket <= 0) break;
        }
    }
    public synchronized void show() { //同步方法
        if(ticket > 0) {              //同步监视器:this
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}
public class ThreadTest03 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        Thread t2 = new Thread(myThread);
        Thread t3 = new Thread(myThread);
        t1.start();
        t2.start();
        t3.start();
    }
}

②解决继承Thread类的线程安全问题

class  MyThread extends Thread {
    private static int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();
            if (ticket <= 0) break;
        }
    }
    public static synchronized void show() { //若不加static,同步监视器:t1、t2、t3
        if(ticket > 0) {      //加上static,同步监视器:MyThread.Class即这个类本身充当对象作为锁(反射机制)
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}
public class ThreadTest03 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

关于同步方法的总结:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
  2. 非静态的同步方法,同步监视器是: this
    静态的同步方法,同步监视器是:当前类本身
3.方式三:Lock锁 —JDK5.0新增

从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

ReentrantLock类实现了Lock ,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

public class ThreadTest06 {
    public static void main(String[] args) {
        Window window = new Window();
        Thread t1 = new Thread(window);
        Thread t2 = new Thread(window);
        Thread t3 = new Thread(window);
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window implements Runnable {
    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        while(true) {
            try{ //使用try-finally代码块是为了让同步代码无论是否执行完最终都要释放锁,否则会出现执行的某一次没有解锁
                //2.调用lock()上锁
                lock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                //3.调用unlock()解锁
                lock.unlock();
            }
        }
    }
}

synchronized和Lock的对比:

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

面试题: synchronized 与Lock的异同?

  • 相同:二者都可以解决线程安全问题

  • 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器

    ​ Lock需要手动的启动同步(上锁Lock() ),同时也需要手动的结束同步(解锁unlock() )

面试题:如何解决线程安全问题?有几种方式?

  • 使用synchronized关键字
    • 使用synchronized修饰代码块,使其成为同步代码块(包住操作共享数据的代码)
    • 使用synchronized修饰方法,使其成为同步方法(方法体是操作共享数据的代码块)
  • 使用Lock锁的方式(常用ReentrantLock类)

7.4.3 线程安全的单例模式之懒汉式

Class Bank {
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance() {
        //方式一:效率稍差
        synchronized(Bnak.class) {
            if(instance == null) {
                instance = new Bank();
            }
            return instance;
         }
        //方式二:效率更高
        if(instance == null) { //第一个if是为了后面的线程可以直接返回instance,而不用进入同步方法块里再进行判断,以此来提高效率
            synchronized(Bnak.class) {
                if(instance == null) {  //这个if不能删除,如果删除仍然存在线程安全问题,因为可能存在多个线程同时调用这个方法,就会同时进入第一个if语句内,这样这些线程就会逃过判空的环节等待进入同步代码块,等到自己使用CPU时又会new一个新的bank对象,那么这个第二个if判断就是第二重保险
                    instance = new Bank();
                }
             }
        }
        return instance;
    }
}

7.4.4 线程的死锁问题

死锁:

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 我们使用同步时要避免出现死锁

死锁产生的四个必要条件:

  • 1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
  • 3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
  • 4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路
  • 当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失

解决方法:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
//死锁实例
public static void main(String[] args) {
    StringBuffer s1 = new StringBuffer();
    StringBuffer s2 = new StringBuffer();

    new Thread(){ //第一个线程
        @Override
        public void run() {
            synchronized (s1) { //以对象s1作为第一把锁
                s1.append("a");
                s2.append("1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (s2) { //以对象s2作为第二把锁
                    s1.append("b");
                    s2.append("2");
                    System.out.println(s1);
                    System.out.println(s2);
                }
            }
        }
    }.start();
    new Thread(new Runnable() { //第二个线程
        @Override
        public void run() {
            synchronized (s2) { ////以对象s2作为第一把锁
                s1.append("c");
                s2.append("3");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (s1) { //以对象s1作为第二把锁
                    s1.append("d");
                    s2.append("4");
                    System.out.println(s1);
                    System.out.println(s2);
                }
            }
        }
    }).start();
}
//该程序形成死锁原因:
//第一个线程执行拿到第一把锁s1,然后会执行sleep进行睡眠,同时第二个线程也开始执行,拿到第一把锁s2
//第一个线程睡眠结束,需要第二把锁s2,但是第二个线程正在占用s2,s2得不到释放,则第一个线程就会一直等待第二个线程释放s2
//但是,此时第二个线程睡眠结束,需要用到锁s1,但第一个线程又因为无法使用锁s2,会一直占用s1导致s1得不到释放,那么第二个线程也就需要等待第一个线程释放锁s1
//由此便产生了第一个线程拿不到s2的同时又不释放s1,而第二个线程拿不到s1的同时也不释放s2,这样就使两个线程都不到互相需要的锁,进入无限循环等待资源的死锁状态

7.5 线程的通信

7.5.1 线程通信的介绍

涉及到的三个方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器

  • notify():一旦执行此方法,就会唤醒被wait()的一个线程。如果有多个线程被wait,就唤醒优先级高的线程

  • notifyAll():一旦执行此方法,就会唤醒**所有被wait()**的线程

说明:

  • wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中
  • wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步代码中的同步监视器,否则,会出现IllegalMonitorStateException异常
  • wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中在·

面试题:sleep()和wait()的异同?

  • 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
  • 不同点:
    • 两个方法声明的位置不同:sleep()声明在Thread类中,wait()声明在object类中
    • 调用的要求不同: sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中
    • 关于**是否释放同步监视器(锁)**:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,而wait()会释放同步监视器
//线程通信的例子:使用两个线程打印1-100,线程1和线程2交替打印
public class ThreadTest08 {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}
class Number implements Runnable {
    private int number = 1;
    @Override
    public void run() {
        while(true) {
            synchronized (this){
                notify(); //唤醒被阻塞的
                if(number <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        wait(); //使调用wait()方法的线程进入阻塞状态,且释放锁(sleep阻塞但不会释放锁)
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

7.5.2 线程通信的应用

经典例题:生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品

这里可能出现两个问题:

  • 生产者比消费者快时,消费者会漏掉一些数据没有取到
  • 消费者比生产者快时,消费者会取同样的数据
/*
生产者和消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),
如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,
如果店中有产品了再通知消费者来取走产品
 */
public class ThreadTest09 {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Productor productor1 = new Productor(clerk); //定义生产者线程
        Productor productor2 = new Productor(clerk);
        Customer customer1 = new Customer(clerk); //定义消费者线程
        Customer customer2 = new Customer(clerk);
        Customer customer3 = new Customer(clerk);
        productor1.setName("生产者1");
        productor2.setName("生产者2");
        customer1.setName("消费者1");
        customer2.setName("消费者2");
        customer3.setName("消费者3");
        productor1.start();
        productor2.start();
        customer1.start();
        customer2.start();
        customer3.start();
    }
}
class Clerk {
    private int product = 0; //店员拥有的产品数
    public int getProduct() {
        return product;
    }
    public void addProduct() {
        product++;
    }
    public void reduceProduct() {
        product--;
    }
}
class Productor extends Thread {
    private Clerk clerk; //声明Clerk对象,方便对于产品数的操作
    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(100); //阻塞线程,让其他线程有争夺的时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(Clerk.class) {
                if(clerk.getProduct() < 20){ //如果商品的数量小于20,则生产者会持续生产
                    System.out.println(Thread.currentThread().getName() + "开始生产......");
                    System.out.println(Thread.currentThread().getName() + "正在准备生产第" + (clerk.getProduct()+1) + "个产品");
                    clerk.addProduct();
                    System.out.println(Thread.currentThread().getName() + "已经完成生产第" + clerk.getProduct() + "个产品");
                    Clerk.class.notify(); //唤醒因一些原因被执行wait()的线程
                } else {
                    //生产者生产过快,店员让生产者等待
                    try {
                        Clerk.class.wait(); //生产者生产太快,商品数量过多,被店员叫停休息
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
class Customer extends Thread {
    private Clerk clerk; //声明Clerk对象,方便对于产品数的操作
    public Customer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(500); //阻塞线程,让其他线程有争夺的时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(Clerk.class) {
                if(clerk.getProduct() > 0){ //如果商品数量大于0,则消费者可以进行消费
                    System.out.println(Thread.currentThread().getName() + "开始消费......");
                    System.out.println(Thread.currentThread().getName() + "正在准备消费第" + clerk.getProduct() + "个产品");
                    System.out.println(Thread.currentThread().getName() + "已经完成消费第" + clerk.getProduct() + "个产品");
                    clerk.reduceProduct();
                    Clerk.class.notify(); //唤醒因一些原因被执行wait()的线程
                } else {
                    //消费者消费过快,店员让消费者等待
                    try {
                        Clerk.class.wait(); //消费者消费过快,商品数量不足,店员告诉消费者稍等
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
/*
此实例实现过程中遇到的问题:
1.因为使用while循环,会导致一个线程一旦抢到CPU使用权后就不易被其他线程抢占,所以在进入同步代码块或同步方法前加上sleep来让其他线程有抢占CPU的机会
2.在同步代码块或同步方法中使用notify()和wait()时要注意调用者一定要是同步监视器,否则会报错IllegalMonitorStateException异常
*/

7.5.3 哪些操作会释放锁,哪些不会释放锁?

1.释放锁的操作
  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
2.不会释放锁的操作
  • 线程执行同步代码块或同步方法时,程序调用**Thread.sleep()Thread.yield()**方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)
    • 应尽量避免使用suspend()和resume()来控制线程

7.6 JDK5新增的线程创建方式

7.6.1 方法一:实现Callable接口

与使用Runnable相比,Callable功能更强大些

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

Future接口:

  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
  • FutrueTask是Futrue接口的唯一的实现类
  • FutureTask同时实现了Runnable,Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
/*
* 实现Callable接口创建线程
*
* 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
* 1. calL()可以有返回值的
* 2. calL()可以抛出异常,被外面的操作捕获,获取异常的信息
* 3. Callable是支持泛型的
*/
//1.创建一个实现Callable接口的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception { //相当于run()方法
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class ThreadTest10 {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象传递到Thread类中,创建线程对象,并调用start()方法
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            //6.获取Callable中call()的返回值(如果需要的话)
            //get()方法的返回值即为FutureTask构造参数Callable实现类重写的call()的返回值
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

7.6.2 方法二:线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关的API:

  • JDK5.0起提供了线程池相关API:ExecutorService和 Executors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
/*
    创建新线程方式四:使用线程池
    面试题:创建多线程的几种方法?四种
 */
class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
public class ThreadTest11 {
    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10); //创建出一个ThreadPoolExecutor类型的线程池,返回一个ExecutorService接口,从源码关系可以看出这儿相当于向上转型
        //如果想设置线程池的一些相关属性,需要将接口对象service向下转成实现类ThreadPoolExecutor对象才能调用其自身拥有的方法
        ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
        service1.setCorePoolSize(15);//比如设置最大连接数
        //2.执行指定的线程的操作,需要提供实现Runnable或Callable接口的实现类的对象
        service.execute(new NumberThread());//适合使用于Runnable //执行的是创建出的线程池类型对应类中的execute()方法
        //service.submit(Callable callable);//适合适用于Callable
        //3.关闭线程池
        service.shutdown();
    }
}

八、Java常用类

8.1 字符串相关的类

8.1.1 String类

1.String的特性
  • String类:代表字符串。Java程序中的所有字符串字面值(如 “abc”)都作为此类的实例实现
  • String是一个final类,代表不可变的字符序列
  • 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改
  • String对象的字符内容是存储在一个字符数组value[]中
  • String实现了serializable接口:表示字符串是支持序列化
  • String实现了Comparable接口:表示String可以比较大小
  • String内部定义了final char[ ] value用于存储字符串数据
  • String:代表不可变的字符序列。简称:不可变性
    • 对字符串重新赋值时,会重写指定内存区域赋值不能使用原有的value进行赋值
    • 对现有的字符串进行连接操作时,也会重新指定内存区域赋值不能使用原有的value进行赋值
    • 当调用String的replace()方法修改指定字符或字符串时,也会重新指定内存区域赋值
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
  • 字符串常量池中是不会存储已有相同内容的字符串的
String s1 = "abc"; //字面量的定义方式
String s2 = "abc";
System.out.println(s1 == s2);//返回true
s2 = "hello";
System.out.println(s1 == s2);//返回false
System.out.println(s1);//abc
System.out.println(s2);//hello
String s3 = "abc";
System.out.print1n(s3);//abc
s3 += "def";
System.out.print1n(s3);//abcdef
String s4 = "abc";
String s5 = s4.replace('a','m');
System.out.println(s4); //abc
System.out.println(s5); //mbc
2.String 对象的创建

面试题:String s = new String(“abc”)方式创建对象,在内存中创建了几个对象?
两个:一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:”abc”

String str = "he1lo"; //字面量定义
String s1 = new String(); //本质上this.value = new char[0];
String s2 = new String(String original); //this.value = original.value;
//this.value = Arrays.copy0f(value, value.length);
String s3 = new String(char[] a);
String s4 = new String(char[] a,int startIndex , int count) ;
Person p1 = new Person("Tom",12);
Person p2 = new Person("Tom",12);
System.out.println(p1.name.equals(p2.name));//true
System.out.println(p1.name == p2.name);//true
//如果想改变名字,如p1.name = "Jerry",还在常量池中又新开辟一个空间存储"Jerry"
3.String不同拼接操作的比较
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop" ;
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop" ;
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
final String s8 = "javaEE";//常量
String s9 = s9 + "hadoop";
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.print1n(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.print1n(s5 == s7);//false
System.out.println(s6 == s7);//false
System.out.println(s3 == s9);//true,s8为final修饰,所以s9是常量加常量,也存储在常量池,所以为true

String s8 = s5.intern();//返回值得到的s8使用的是常量池中已经存在的javaEEhadoop"
//str5调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用
System.out.println(s3 == s8);//true

结论:

  • 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量
  • 只要其中有一个是变量,结果就在堆中
  • 如果拼接的结果调用intern(方法,返回值就在常量池中
//练习:面试题
String str = new String("good"); //此时str指向堆中的new String("good")
char[] ch = { 't', 'e', 's', 't' };//同样,ch指向堆中的test
public void change(String str, char ch[]) {//传入str和ch指向的对象的引用值
    str = "test ok"; //此时是字面量创建,所以会创建在常量池中,但change外的str指向并没有变,所以输出仍是good
    ch[0] = 'b';//根据传入的数组的引用值,将其ch[0]的值改变了,所以输出best
public static void main(String[] args) {
    StringTest ex = new StringTest();
    ex.change(ex.str, ex.ch);
    System.out.println(ex.str);//good
    System.out.println(ex.ch);//best
}
4.JVM中涉及字符串的内存结构

JDK1.6:字符串常量池在方法区(具体实现:永生代)中

JDK1.7:字符串常量池在堆中

JDK1.8:字符串常量池在方法区(具体实现:元空间)中

5.String常用方法
  • int length():返回字符串的长度: return value.length

  • char charAt(int index):返回某索引处的字符return value[index]

  • boolean isEmpty():判断是否是空字符串: return value.length == 0

  • String tol owerCase():使用默认语言环境,将String中的所有字符转换为小写

  • String toUpperCase():使用默认语言环境,将String中的所有字符转换为大写

  • String trim():返回字符串的副本,忽略前导空白和尾部空白

  • boolean equals(Object obj):比较字符串的内容是否相同

  • boolean equalslgnoreCase(String anotherString):与equals方法类似, 忽略大小写

  • String concat(String str):将指定字符串连接到此字符串的结尾。等价于用 “+”

  • int compare To(String anotherString):比较两个字符串的大小

  • String substring(int beginlndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串

  • String substring(int beginIndex, int endIndex):返回一个新字符串,它是此字符串从beginlndex开始截取到endIndex(不包含)的一个子字符串

  • String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用newChar替换此字符串中出现的所有oldChar得到的

  • String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串

  • String replaceAll(String regex, String replacement):使用给定的replacement替换此字符串所有匹配给定的正则表达式的子字符串

  • String replaceFirst(String regex, String replacement):使用给定的replacement替换此字符串匹配给定的正则表达式的第一个子字符串

  • boolean matches(String regex):告知此字符串是否匹配给定的正则表达式

  • String[] split(String regex):根据给定正则表达式的匹配拆分此字符串

  • String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中

  • boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束

  • boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始

  • boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始

  • boolean contains(CharSequence s):当且仅当此字符串包含指定的char值序列时,返回true

    //调用此方法的字符串是否含有传入的字符串
    String str1 = "helloworld";
    String str2 = "wo";
    System.out.println(str1.contains(str2));//true
    
  • int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引

  • int indexOf(String str, int fromlndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始

  • int lastlndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引

  • int lastlndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索

  • 注:indexOf和lastIndexOf方法如果未找到都是返回-1,如果只出现一次该子字符串,返回值相同

6.String与其他结构之间的转换
①String与基本数据类型、包装类之间的转换
//String -->基本数据类型、包装类:调用包装类的静态方法: parseXx(str)
String str1 = "123";
int num = (int)str1;//错误的
int num = Integer.parseInt(str1);
//基本数据类型、包装类--> String: 调用String重载的value0f(xxx)
String str2 = String.valueOf(num);//"123"
String str3 = num + "";
System.out.println(str1 == str3);//false,str1在常量池中,str3在堆中
②String与char[]之间的转换
//String --> char[]: 调用String的toCharArray()
String str1 = "abc123";
char[] charArray = str1. toCharArray();
for (int i = 0; i < charArray.1ength; i++) {
    System.out.print1n( charArray[i]);
}
//char[] --> String: 调用String的构造器
char[] arr = new char[]{'h','e','1','1','o'};
String str2 = new String(arr);
System.out.println(str2);
③String与byte[]之间的转换
//编码:字符串-->字节 (看得懂 --->  看不懂的二进制数据)
//解码:字节-->字符串(看不懂的二进制数据 --->看得懂)
//编码: String --> byte[]:调用String的getBytes()
//说明:解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码
String str1 = "abc123中国";
byte[] bytes = str1.getBytes();//使用默认的字符集,进行转换
System.out.println(Arrays.toString(bytes));
byte[] gbks = str1.getBytes(charsetName: "gbk" );//使用gbk字符集进行编码
System.out.println(Arrays.toString(gbks));
//解码: byte[] --> String:调用String的构造器
String str2 = new String(bytes);//使用默认的字符集(UTF-8),进行解码
System. out. println(str2);
String str3 = new String(gbks) ;
System.out.println(str3);//出现乱码。原因:编码集和解码集不一致
7.String常见的算法题目
  1. 模拟一个trim()方法,去除字符串两端的空格
  2. 将一个字符串进行反转。将字符串中指定部分进行反转。比如“abcdefg”反转为”abfedcg”
  3. 获取一个字符串在另一个字符串中出现的次数,比如:获取”ab”在”abkkcadkabkebfkabkskab”中出现的次数
  4. 获取两个字符串中最大相同子串。比如:str1 = “abcwerthelloyuiodef”;str2 = “cvhellobnm”
    提示:将短的那个串进行长度依次递减的子串与较长的串比较
  5. 对字符串中字符进行自然顺序排序
    提示:①字符串变成字符数组 ②对数组排序,选择,冒泡,Arrays sor() ③将排序后的数组变成字符串

8.1.2 StringBuffer和StringBuilder

1.StringBuffer类

java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象

很多方法都和String相同

作为参数传递时,方法内部可以改变值

在StringBuffer类中char[] value没有用final声明,value可以不断扩容;int count有效字符的个数

//源码分析
String str = new String();// <=> char[] value = new char[0];
String str1 = new String( "abc");// <=> char[] value = new char[]{'a', 'b','c'};
StringBuffer sb1 = new StringBuffer();// <=> char[] value = new char[16]; 底层创建了一个长度是16的字符数组
System.out.println(sb1.Length());//输出0,因为该类的length()方法是返回数组中有效字符的个数即return count;
sb1.append('a');//value[0] = 'a';
sb1.append('b')://value[1] = 'b';
StringBuffer sb2 = new StringBuffer( "abc");//char[] value = new char["abc".Length()+16]
//问题1. System.out.println(sb2.Length());//3
//问题2.扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。
//默认情况下,扩容为原来容量的2倍+ 2,同时将原有数组中的元素复制到新的数组中。
//指导意义:开发中建议大家使用: StringBuffer(int capacity) 或StringBuilder(int capacity)
String str = nu1l;
StringBuffer sb = new StringBuffer();
sb.append(str);//源码抽象类AbstractStringBuilder中会判断str是否为null,如果是则会调用appendNull()方法将null当作字符串拼接在sb后面
System.out.println(sb.1ength());//输出4
System.out.print1n(sb);//输出字符串"null"
StringBuffer sb1 = new StringBuffer(str);//抛出NullPointerException异常
System.out.println(sb1);
2.StringBuilder类

StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样

面试题:对比String、StringBuffer、 StringBuilder三者的异同
String(JDK1.0):不可变字符序列
StringBuffer(JDK1.0):可变字符序列、效率低线程安全
StringBuilder(JDK5.0):可变字符序列、效率高线程不安全

注意:作为参数传递的话,方法内部String不会改变其值StringBuffer和StringBuilder会改变其值

3.StringBuffer和StringBuilder中的一些方法:
  • StringBuffer append(xxx):提供了很多的append()方法, 用于进行字符串拼接
  • StringBuffer delete(int start,int end):删除指定位置的内容
  • StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
  • StringBuffer insert(int offset, xxX):在指定位置插入xxx
  • StringBuffer reverse():把当前字符序列逆转
  • public int index0f(String str)
  • public String substring(int start, int end)
  • public int Length( )
  • public char charAt(int n)
  • public void setCharAt(int n , char ch)

总结:

增:append(xxx)
删:delete(int start, int end)
改:setCharAt(int n ,char ch) / replace(int start, int end, String str)
查:charAt(int n )
插:insert(int offset, xxx) .
长度:Length();
遍历:for() + charAt() / toString()

4.String、StringBuffer、StringBuilder三者的效率对比
//初始设置
long startTime = 0L;
long endTime = 0L;
String text = "";
StringBuffer buffer = new StringBuffer("");
StringBuilder builder = new StringBuilder("");
//开始对比
startTime = System.currentTimeMillis();
for(int i=0; i<20000; i++){
    buffer.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer的执行时间: " + (endTime - startTime));
startTime = System.currentTimeMillis();
for(int i=0; i<20000; i++){
    builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder的执行时间: " + (endTime - startTime));
startTime = System.currentTimeMillis();
for(int i=0; i<20000; i++){
    text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String的执行时间: " + (endTime - startTime));
//StringBuilder>StringBuffer>String

8.2 日期时间相关的类

8.2.1 java.lang.System类(JDK8之前)

System类提供的public static long currentTimeMillis()用来返回当前时间1970年1月1日0时0分0秒之间以毫秒为单位的时间差(此方法适于计算时间差)

long time = System.currentTimeMillis();
//返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差
//称为时间戳
System.out.println(time);

计算世界时间的主要标准有:

  • UTC(Coordinated Universal Time)
  • GMT(Greenwich Mean Time)
  • CST(Central Standard Time)

8.2.2 java.util.Date类(JDK8之前)

表示特定的瞬间,精确到毫秒

构造器:

  • Date():使用无参构造器创建的对象可以获取本地当前时间
  • Date(long date)

常用方法:

  • getTime():返回自 1970 年1月1日00:00:00 GMT 以来此Date对象表示的毫秒数
  • toString():把此 Date对象转换为以下形式的String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz是时间标准
  • 其它很多方法都过时了
//构造器一:Date(),返回当前时间的Date对象
Date date = new Date();
System.out.println(date.toString());//Fri Jul 30 11:20:58 CST 2021
System.out.println(date.getTime());//1627615258490
//构造器二:Date(long date),返回指定毫秒数的Date对象
Date date1 = new Date(372846236919L);
System.out.println(date1);//Sun Oct 25 16:23:56 CST 1981

java.sql.Date对应着数据库中的日期类型的变量

  • 如何实例化
java.sql.Date date2 = new java.sql.Date(9837592502580L);
System.out.println(date2);//2281-09-28
  • 如何将java.util.Date对象转换为java.sql.Date对象
//情况一:
Date date4 = new java.sql.Date(2343243242323L); //子类转父类
java.sql.Date date5 = (java.sql.Date)date4; //可以赋值给子类对象
//情况二:
Date date6 = new Date();
java.sq1.Date date7 = (java.sql.Date)date6 //父类直接向下转成子类,报错
java.sq1.Date date7 = new java.sq1.Date(date6.getTime()); //先将获得父类对象的毫秒数再利用构造器转换成sql中的日期

8.2.3 java.text.SimpleDateFormat类(JDK8之前)

Date类的API不易于国际化,大部分被废弃了,java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类

它允许进行格式化:日期 -> 文本、 解析:文本 -> 日期

格式化:
SimpleDateFormat():默认的模式和语言环境创建对象
public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象, 该对象调用:
public String format(Date date)方法格式化时间对象date

解析:public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期

//使用默认构造器实例化SimpleDateFormat对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
//格式化:日期 --> 字符串
Date date = new Date();
String format = simpleDateFormat.format(date);
System.out.println(format);
//解析:字符串 --> 日期
String str = "21-7-31 上午10:23";//使用SimpleDateFormat默认构造器实例化的对象必须使用这种这种格式,否则会抛出异常
Date date1 = simpleDateFormat.parse(str);
System.out.println(date1);
//按照指定格式进行格式化和解析:调用带参的构造器
System.out.println("============指定格式==============");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//传入的参数即为日期要格式化和解析的格式
//格式化
Date date2 = new Date();
String format1 = sdf.format(date2);
System.out.println(format1);
//解析:要求字符串必须是符合SimpleDateFormat的格式
String str1 = "2021-07-31 10:34:45";
Date date3 = sdf.parse(str1);
System.out.println(date3);

8.2.4 java.utils.Calendar(日历)类(JDK8之前)

Calendar是一个抽象基类,主用用于完成日期字段之间相互操作的功能

获取Calendar实例的方法:

  • 使用**Calendar.getInstance()**方法
  • 调用它的子类GregorianCalendar的构造器

一个Calendar的实例是系统时间的抽象表示,通过get(int field)方法来取得想要的时间信息。比如YEAR、MONTH、DAY_OF_ _WEEK、HOUR_OF_DAY 、MINUTE、SECOND

  • public void set(int field,int value)
  • public void add(int field,int amount)
  • public final Date getTime()
  • public final void setTime(Date date)

注意:

  • 获取月份时:一月是0,二月是1,以此类推,12月是11
  • 获取星期时:周日是1,周二是2,。。。。周六是7
//1.实例化
//方式一:创建其子类(GregorianCalendar)的对象
//方式二:调用其静态方法getInstance()
Calendar calendar = Calendar.getInstance();//返回的其实还是子类GregorianCalendar的对象

//2.常用方法
int days = calendar.get(Calendar.DAY_OF_MONTH);//get(),获取当前calendar对象对应字段的值
System.out.println(days);
System.out.println("==============");
calendar.set(Calendar.DAY_OF_MONTH,22);//set(),设置当前calendar对象的属性字段值,
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
System.out.println("==============");
calendar.add(Calendar.DAY_OF_MONTH,4);//add(),在当前calendar对象对应字段值的基础上加减
System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
System.out.println("==============");
Date date = calendar.getTime();//getTime(),日历类 --> Date
System.out.println(date);
System.out.println("==============");
Date date1 = new Date();
calendar.setTime(date1);//setTime(),Date --> 日历类
System.out.println(calendar.get(Calendar.DAY_OF_MONTH));

8.2.5 java.time(JDK8中)

Java8吸收了Joda-Time的精华,以一个新的开始为Java创建优秀的API

新的java.time中包含了所有关于本地日期(LocalDate)本地时间(LocalTime)本地日期时间(LocalDateTime)时区( ZonedDate Time)持续时间(Duration)的类。历史悠久的Date类新增了tolnstant() 方法, 用于把Date转换成新的表示形式

LocalDate、LocalTime、LocalDateTime类是其中较重要的几个类,它们的实例是不可变的对象,分别表示使用ISO-8601日历系统的日期时间日期和时间。它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息

  • LocalDate代表IOS格式(yyyy-MM-dd) 的日期,可以存储生日、纪念日等日期
  • LocalTime表示一个时间, 而不是日期
  • LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一

注:ISO-8601日历系统是国际标准化组织制定的现代公民的日期和时间的表示法,也就是公历

1.LocalDate、LocalTime、LocalDateTime类
方法 描述
now() / * now(Zoneld zone) 静态方法,根据当前时间创建对象/指定时区的对象
of() 静态方法,根据指定日期/时间创建对象
getDayOfMonth()/getDayOfYear() 获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek() 获得星期几(返回一个DayOfWeek枚举值)
getMonth() 获得月份,返回一个Month枚举值
getMonthValue()/getYear() 获得月份(1-12)/获得年份
getHour()/getMinute()/getSecond() 获得当前对象对应的小时、分钟、秒
withDayOfMonth()/withDayOfYear()/withMonth()/withYear() 将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
plusDays(), plusWeeks(),plusMonths(), plusYears(),plusHours() 向当前对象添加几天、几周、几个月、几年、几小时
minusMonths()/minusWeeks()/minusDays()/minus Years()/minusHours 从当前对象减去几月、几周、几天、几年、几小时
//jdk8中的时间API
//LocalDate、LocalTime、LocalDateTime获取当前的日期、时间、日期+时间,LocalDateTime使用最多
//方法一:now()实例化对象
LocalDate now = LocalDate.now();
LocalTime now1 = LocalTime.now();
LocalDateTime now2 = LocalDateTime.now();
System.out.println(now);
System.out.println(now1);
System.out.println(now2);
//方法二:of()实例化对象,可指定具体的日期和时间年、月、日、时、分、秒,没有偏移量
LocalDateTime of = LocalDateTime.of(2021, 7, 31, 16, 30, 34);
System.out.println(of);
//getXxx()
System.out.println(of.getDayOfMonth());
System.out.println(of.getDayOfWeek());
System.out.println(of.getMonth());
System.out.println(of.getMonthValue());
System.out.println(of.getMinute());
//withXxxx(),返回新的对象,原对象仍然保存,体现出不可变性
LocalDateTime localDateTime = of.withDayOfMonth(20);
System.out.println(localDateTime);
//plusXxxx(),返回新的对象,原对象仍然保存,体现出不可变性
LocalDateTime localDateTime1 = of.plusMonths(2);
System.out.println(localDateTime1);
//minusXxxx(),返回新的对象,原对象仍然保存,体现出不可变性
LocalDateTime localDateTime2 = of.minusHours(2);
System.out.println(localDateTime2);
2.Instant类

Instant:时间线上的一个瞬时点。这可能被用来记录应用程序中的事件时间戳

在处理时间和日期的时候,我们通常会想到年,月,日,时,分,秒。然而,这只是时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连续的。在此模型中,时间线中的一个点表示为一个很大的数,这有利于计算机处理。在UNIX中,这个数从1970年开始, 以秒为的单位;同样的,在Java中,也是从1970年开始,但以毫秒为单位

javatime包通过值类型Instant提供机器视图,不提供处理人类意义上的时间单位。Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级

(1ns= 10^-9s) 1秒= 1000毫秒=10^6微秒=10^9纳秒

方法 描述
now() 静态方法,返回默认UTC时区的Instant类的对象
ofEpochMill(long epochMili) 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
atOffset(ZoneOffset offset) 结合即时的偏移来创建一个OffsetDateTime
toEpochMilli() 返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数

//now(),本初子午线的标准时间
Instant now = Instant.now();
System.out.println(now);//2021-07-31T09:15:43.844Z
//atOffset(),添加时间偏移量
OffsetDateTime offsetDateTime = now.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);//2021-07-31T17:15:43.844+08:00
//toEpochMilli(),获取自1970年1月1日0时0分0秒(UTC)开始到当前的毫秒数,类似于getTime()
long milli = now.toEpochMilli();
System.out.println(milli);//1627722943844
//ofEpochMilli(),创建在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象,类似于new Date(long milli)
Instant instant = Instant.ofEpochMilli(1627722943844L);
System.out.println(instant);//2021-07-31T09:15:43.844Z
3.java.time.format.DateTimeFormpatter类

java.time.format.DateTimeFormpatter类类似于SimpleDateFormat类,都是格式化或解析日期、时间的类

该类提供了三种格式化方法:

  • 预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
  • 本地化相关的格式。 如:ofLocalizedDateTime(FormatStyle.LONG/FormatStyle.MEDIUM/FormatStyLe.SHORT)、ofLocalizedDate(FormatStyLe.FULL/FormatStyLe.LONG/FormatStyLe.MEDIUM/FormatStyLe.SHORT)
  • 自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss E”)
方法 描述
ofPattern(String pattern) 静态方法,返回一个指定字符串格式的DateTimeFormatter
format(TemporalAccessor t) 格式化一个日期、 时间,返回字符串
parse(CharSequence text) 将指定格式的字符序列解析为一个日期、时间
//方法一:预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
//格式化:日期 ---> 字符串
LocalDateTime now = LocalDateTime.now();
String str = formatter.format(now);
System.out.println(now);//2021-07-31T18:00:04.243
System.out.println(str);//2021-07-31T18:00:04.243
//解析:字符串 ---> 日期
TemporalAccessor parse = formatter.parse("2021-07-31T17:38:27.063");
System.out.println(parse);//{},ISO resolved to 2021-07-31T17:38:27.063
//方法二:
//本地化相关的格式。 如:ofLocalizedDateTime(FormatStyle.LONG)
LocalDateTime now1 = LocalDateTime.now();
DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
//格式化
String str1 = formatter1.format(now1);
System.out.println(now1);//2021-07-31T18:00:04.251
System.out.println(str1);//2021年7月31日 下午06时00分04秒
//解析
TemporalAccessor parse1 = formatter1.parse("2021年7月31日 下午05时52分08秒");
System.out.println(parse1);//{},ISO resolved to 2021-07-31T17:52:08
//本地化相关的格式。 如:ofLocalizedDate(FormatStyle.FULL)
LocalDate now2 = LocalDate.now();
DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
//格式化
String str2 = formatter2.format(now2);
System.out.println(now2);//2021-07-31
System.out.println(str2);//2021年7月31日 星期六
//解析
TemporalAccessor parse2 = formatter2.parse("2021年7月31日 星期六");
System.out.println(parse2);//{},ISO resolved to 2021-07-31
//方法三:重点:自定义的格式。如:ofPattern("yyyy-MM-dd hh:mm:ss E")
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
//格式化
String str3 = formatter3.format(LocalDateTime.now());
System.out.println(str3);//2021-07-31 06:06:53
//解析
TemporalAccessor parse3 = formatter3.parse("2021-07-31 06:06:37");
System.out.println(parse3);//{SecondOfMinute=37, HourOfAmPm=6, MicroOfSecond=0, NanoOfSecond=0, MinuteOfHour=6, MilliOfSecond=0},ISO resolved to 2021-07-31
4.其他API

ZoneId:该类中包含了所有的时区信息,一个时区的ID,如Europe/Paris

ZonedDateTime:一个在IS0-8601日历系统时区的日期时间,如2007-12-03T10:15:30+01:00 Europe/Paris

  • 其中每个时区都对应着ID,地区ID都为“{区域}{城市}”的格式,例如:Asia/Shanghai等

Clock:使用时区提供对当前即时、日期和时间的访问的时钟

  • 持续时间: Duration, 用于计算两个“时间”间隔
  • 日期间隔: Period, 用于计算两个“日期”间隔

TemporalAdjuster:时间校正器。有时我们可能需要获取例如:将日期调整到“下一个工作日”等操作

TemporalAdjusters:该类通过静态方法(firstDayOfXxx()/lastDayOfXxx()/nextXxx())提供了大量的常用TemporalAdjuster的实现

8.3 Java比较器

Java实现对象排序的方式有两种:

  • 自然排序:java.lang.Comparable
  • 定制排序:java.util.Comparator

说明:Java中的对象,正常情况下,只能进行比较:==或!=。不能使用>或<的,但是在开发场景中,我们需要对多个对象进行排序,言外之意,就需要比较对象的大小

如何实现呢?使用两个接口中的任何一个: Comparable 或Comparator

8.3.1 Comparable接口(自然排序)

Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序

实现Comparable的类必须实现compare To(Object obj)方法,两个对象即通过compareTo(Object obj)方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零

实现Comparable接口的对象列表(和数组)可以通过Collections.sort或Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器

对于类C的每一个e1和e2来说,当且仪当e1.compareTo(e2) == 0与e1.equals(e2)具有相同的boolean值时,类C的自然排序才叫做与equals一致。建议(虽然不是必需的)最好使自然排序与equals一致

//Comparable接口的使用实例:
//1.String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较两个对象大小的方法
//2.String、包装类重写compareTo()方法以后,进行了从小到大的排列
//3.重写compareTo(obj)的规则:
//  如果当前对象上his大于形参对象obj,则返回正整数,
//  如果当前对象this小于形参对象obj,则返回负整数,
//  如果当前对象this等于形参对象obj,则返回零。
String[] arr = new String[]{"AA","CC","KK","MM","GG","JJ","DD"};
Arrays.sort(arr);//sort方法底层是通过Comparable接口实现的,即传入数组的元素的类型必须要实现Comparable接口,然后会调用ComparableTo()方法进行各元素间的比较
System.out.println(Arrays.toString(arr));//[AA, CC, DD, GG, JJ, KK, MM]
//4.对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compareTo(obj)方法,在方法中指明如何排序,从另一方面说明了需要进行比较的数组元素类型对应的类必须实现Comparable接口
public class Goods implements Comparable {
    private String name;
    private double price;
    public Goods() {
    }
    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
    //指明商品比较大小的方式:按价格从低到高
    @Override
    public int compareTo(Object o) {
        if(o instanceof Goods) {
            Goods goods = (Goods) o;
            //方式一:
            if(this.price > goods.price)
                return 1;
            else if(this.price < goods.price)
                return -1;
            else
                return 0;
            //方式二:
            //return Double.compare(this.price,goods.price);
        }
        throw new RuntimeException("传入的数据类型不正确");
    }
}
main(){
    Goods[] goods = new Goods[4];
    goods[0] = new Goods("lenovoMouse",34);
    goods[1] = new Goods("dellMouse",43);
    goods[2] = new Goods("xiaomiMouse",20);
    goods[3] = new Goods("huaweiMouse",65);
    Arrays.sort(goods);
    System.out.println(Arrays.toString(goods));//[Goods{name='xiaomiMouse', price=20.0}, Goods{name='lenovoMouse', price=34.0}, Goods{name='dellMouse', price=43.0}, Goods{name='huaweiMouse', price=65.0}]
}

8.3.2 Comparator接口(定制排序)

当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用Comparator 的对象来排序,强行对多个对象进行整体排序的比较

重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2

可以将Comparator传递给sort方法(如Collections.sort或Arrays.sort),从而允许在排序顺序上实现精确控制

还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序

//Comparator接口
//String类测试
String[] arr = new String[]{"AA","CC","KK","MM","GG","JJ","DD"};
Arrays.sort(arr,new Comparator(){
    //字符串从大到小进行排列
    @Override
    public int compare(Object o1, Object o2) {
        if(o1 instanceof String && o2 instanceof String) {
            String s1 = (String) o1;
            String s2 = (String) o2;
            return -s1.compareTo(s2);
        }
        throw new RuntimeException("输入的数据类型不一致");
    }
});
System.out.println(Arrays.toString(arr));//[MM, KK, JJ, GG, DD, CC, AA]
//自定义类测试
Goods[] goods = new Goods[5];
goods[0] = new Goods("lenovoMouse",34);
goods[1] = new Goods("dellMouse",43);
goods[2] = new Goods("xiaomiMouse",20);
goods[3] = new Goods("huaweiMouse",65);
goods[4] = new Goods("huaweiMouse",165);
Arrays.sort(goods,new Comparator() {
    //商品先按商品名从低到高排,如果商品名一样则按价格从高到低排
    @Override
    public int compare(Object o1, Object o2) {
        if(o1 instanceof Goods && o2 instanceof Goods) {
            Goods goods1 = (Goods) o1;
            Goods goods2 = (Goods) o2;
            if(goods1.getName().equals(goods2.getName())){
                return -Double.compare(goods1.getPrice(),goods2.getPrice());//价格从高到低排
            }else{
                return goods1.getName().compareTo(goods2.getName());//商品名字从低到高排序
            }
        }
        throw new RuntimeException("比较的类型不一样");
    }
});
System.out.println(Arrays.toString(goods));//[Goods{name='dellMouse', price=43.0}, Goods{name='huaweiMouse', price=165.0}, Goods{name='huaweiMouse', price=65.0}, Goods{name='lenovoMouse', price=34.0}, Goods{name='xiaomiMouse', price=20.0}]

8.3.3 Comparable接口与Comparator接口的简单对比

Comparable接口的方式一旦一定,保证Comparable接口实现类的对象在任何位置都可以比较大小

Comparator接口属于临时性的比较

8.4 System类

System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包

由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。

成员变量

  • System类内部包含in、out和err三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)

成员方法

  • native long currentTimeMillis():该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数

  • void exit(int status):该方法的作用是退出程序。其中status的值为0代表正常退出非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能

  • void gc():该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况

  • String getProperty(String key):该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:

    属性名 属性说明
    java.version Java运行时环境版本
    java.home Java安装目录
    os.name 操作系统的名称
    os.version 操作系统的版本
    user.name 用户的账户名称
    user.home 用户的主目录
    user.dir 用户的当前工作目录
    String javaVersion = System.getProperty("java.version");
    System.out.println("java的version:" + javaVersion);//java的version:1.8.0_291
    String javaHome = System.getProperty("java.home");
    System.out.println("java的home:" + javaHome);//java的home:C:\ProgramFiles\Java\jdk1.8.0_291\jre
    String osName = System.getProperty("os.name");
    System.out.println("os的name:" + osName );//os的name:Windows 10
    String osVersion = System.getProperty("os.version");
    System.out.println("os的version:" + osVersion);//os的version: 10.0
    String userName = System.getProperty("user.name" );
    System.out.println("user的name:" + userName);//user的name:Administrator
    String userHome = System.getProperty("user.home");
    System.out.println("user的home:" + userHome);//user的home:C:\Users\Administrator
    String userDir = System.getProperty("user.dir");
    System.out.println("user的dir:" + userDir);//user的dir:E:\JavaProject\IDEAProject\自主练习\Demo
    

8.5 Math类

java.lang.Math提供了一系列静态方法用于科学计算。其方法的参数和返回值类型一般为double型

方法名 用法
abs 绝对值
acos,asin,atan,cos,sin,tan 三角函数
sqrt 平方根
pow(double a,doble b) a的b次幂
log 自然对数
exp e为底指数
max(double a,double b) 取a与b之间的最大值
min(double a,double b) 取a与b之间的最小值
random() 返回0.0到1.0的随机数
long round(double a) double型数据a转换为long型(四舍五入)
toDegrees(double angrad) 弧度 –> 角度
toRadians(double angdeg) 角度 –> 弧度

8.6 BigInteger与BigDecimal

8.6.1 BigInteger类

java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger提供所有Java的基本整数操作符的对应物,并提供java.lang.Math的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD计算、质数测试、素数生成、位操作以及一些其他操作

构造器:BigInteger(String val):根据字符串构建BigInteger对象

常用方法:

  • public BigInteger abs():返回此BigInteger的绝对值的BigInteger
  • BigInteger add(BigInteger val):返回其值为**(this + val)**的BigInteger
  • BigInteger subtract(BigInteger val):返回其值为**(this - val)**的BigInteger
  • BigInteger multiply(BigInteger val):返回其值为**(this * val)**的BigInteger
  • BigInteger divide(BigInteger val):返回其值为**(this / val)的BigInteger。整数相除只保留整数部分**
  • BigInteger remainder(BigInteger val):返回其值为**(this % val)**的BigInteger
  • BigInteger[] divideAndRemainder(BigInteger val):返回包含**(this / val)后跟(this % val)**的两个BigInteger的数组
  • BigInteger pow(int exponent):返回其值为**(this^exponent)**的BigInteger

8.6.2 BigDecimal类

一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到java.math.BigDecimal类

BigDecimal类支持不可变的任意精度的有符号十进制定点数

构造器

  • public BigDecimal(double val)
  • public BigDecimal(String val)

常用方法

  • public BigDecimal add(BigDecimal augend)
  • public BigDecimal subtract(BigDecimal subtrahend)
  • public BigDecimal multiply(BigDecimal multiplicand)
  • public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
BigInteger bi = new BigInteger("12433241123");
BigDecimal bd = new BigDecimal("12435.351");
BigDecimal bd2 = new BigDecimal("11");
System.out.println(bi);//12433241123
//System.out.println(bd.divide(bd2)); //除不尽就必须要告诉保留多少位
System.out.println(bd.divide(bd2,BigDecimal.ROUND_HALF_UP));//1130.486
System.out.println(bd.divide(bd2,15,BigDecimal.ROUND_HALF_UP));//1130.486454545454545

九、枚举类和注解

9.1 枚举(enum)

Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。当需要定义一组常量时,强烈建议使用枚举类

Java 枚举类使用 enum 关键字来定义,类的对象只有有限个,确定的各个常量使用逗号 , 来分割

JDK5之前自定义一个季节枚举类

class Season{
    //1.声明Season属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;
    //2.私有化类的构造器,并给对象赋值
    private Season(String seasonName,String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    //3.提供当前枚举类的多个对象
    public static final Season SPRING = new Season("春天" ,"春暖花开");
    public static final Season SUMMER = new Season("夏天","夏日炎炎");
    public static final Season AUTUMN = new Season("秋天" ,"秋高气爽");
    public static final Season WINTER = new Season("冬天" ,"冰天雪地");
    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
    //其他诉求2:提供toString()方法
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}

JDK5之后自定义一个季节枚举类

//使用enum定义的枚举类默认继承java.lang.Enum类
enum Season  { 
    //1.提供当前枚举类的对象,多个对象之间用","隔开,最后用";"结束
    SPRING("春天" ,"春暖花开"),
    SUMMER("夏天","夏日炎炎"),
    AUTUMN("秋天" ,"秋高气爽"),
    WINTER("冬天" ,"冰天雪地");
    //2.声明Season属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;
    //3.私有化类的构造器,并给对象赋值
    private Season(String seasonName,String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
    //其他诉求2:提供toString()方法,此时的toString方法可不提供,因为这个枚举类继承自Enum类,打印输出的仍是这个枚举类对象的对象名
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    } 
} 

9.1.1 枚举类成员

枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用

枚举类既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它

enum Color {
    RED, GREEN, BLUE;
    // 构造函数
    private Color() {
        System.out.println("Constructor called for : " + this.toString());
    }
    public void colorInfo() {
        System.out.println("Universal Color");
    }
}
 
public class Test {    
    // 输出
    public static void main(String[] args) {
        Color c1 = Color.RED;
        System.out.println(c1);
        c1.colorInfo();
    }
}

枚举类中抽象方法的实现:

enum Color {
    RED {
        public String getColor(){//枚举对象实现抽象方法
            return "红色";
        }
    },
    GREEN{
        public String getColor(){//枚举对象实现抽象方法
            return "绿色";
        }
    },
    BLUE{
        public String getColor(){//枚举对象实现抽象方法
            return "蓝色";
        }
    };
    public abstract String getColor();//定义抽象方法
}

9.1.2 values(),ordinal()和valueOf()方法

enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Seriablizable 和 java.lang.Comparable 两个接口。

values(), ordinal() 和 valueOf() 方法位于 java.lang.Enum 类中:

  • values():枚举类中所有的值
  • ordinal():可以找到每个枚举常量的索引,就像数组索引一样
  • valueOf():返回指定字符串值的枚举常量
  • toString():返回当前枚举类对象常量的名称
enum Color {
    RED, GREEN, BLUE;
}
public class Test {
    public static void main(String[] args) {
        // 调用 values()
        Color[] arr = Color.values();
        // 迭代枚举
        for (Color col : arr) {
            // 查看索引
            System.out.println(col + " at index " + col.ordinal());
        }
        // 使用 valueOf() 返回枚举常量,不存在的会报错 IllegalArgumentException
        System.out.println(Color.valueOf("RED"));
        // System.out.println(Color.valueOf("WHITE"));
    }
}

9.1.3 使用Enum关键字定义的枚举类实现接口

情况一:实现接口,在enum类中实现抽象方法

情况二:让枚举类的对象分别实现接口中的抽象方法

interface Info{
    void show();
}
enum Season implements Info{
    //枚举类实现接口情况一:实现接口中的抽象方法
    @Override
    public void show() {
        System.out.println("这一个季节");
    }
    //枚举类实现接口情况二:每个枚举类对象独立实现抽象方法
    SPRING("春天" ,"春暖花开"){
        @Override
        public void show() {
            System.out.println("这是春天");
        }
    },
    SUMMER("夏天","夏日炎炎"){
        @Override
        public void show() {
            System.out.println("这是夏天");
        }
    },
    AUTUMN("秋天" ,"秋高气爽"){
        @Override
        public void show() {
            System.out.println("这是秋天");
        }
    },
    WINTER("冬天" ,"冰天雪地"){
        @Override
        public void show() {
            System.out.println("这是冬天");
        }
    };
}

9.1.4 内部类中使用枚举

枚举类可以声明在内部类中:

public class Test {
    enum Color {
        RED, GREEN, BLUE;
    }
 
    // 执行输出结果
    public static void main(String[] args) {
        Color c1 = Color.RED;
        System.out.println(c1); //输出为RED
    }
}

每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的。

以上的枚举类 Color 转化在内部类实现:

class Color {
     public static final Color RED = new Color();
     public static final Color BLUE = new Color();
     public static final Color GREEN = new Color();
}

9.1.5 迭代枚举元素

可以使用for语句来迭代枚举元素:

enum Color {
    RED, GREEN, BLUE;
}
public class MyClass {
  public static void main(String[] args) {
    for (Color myVar : Color.values()) {
      System.out.println(myVar);
    }
  }
}

9.1.6 在switch中使用枚举类

枚举类常运用于switch语句中:

enum Color {
    RED, GREEN, BLUE;
}
public class MyClass {
  public static void main(String[] args) {
    Color myVar = Color.BLUE;
    switch(myVar) {
      case RED:
        System.out.println("红色");
        break;
      case GREEN:
         System.out.println("绿色");
        break;
      case BLUE:
        System.out.println("蓝色");
        break;
    }
  }
}

9.2 注解(Annotation)

从JDK5.0开始,Java增加了对元数据(MetaData)的支持,也就是Annotation(注解)

Annotation其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过使用Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署

Annotation可以像修饰符一样被使用,可用于修饰包,类,构造器,方法,成员变量,参数,局部变量的声明,这些信息被保存在Annotation的“name=value”对中

JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码和XML配置等

未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的Struts2有一部分也是基于注解的了,注解是一种趋势,一定程度上可以说:框架=注解+反射+设计模式

9.2.1 常见的Annotation示例

使用Annotation时要在其前面增加**@**符号,并把该Annotation当成一个修饰符使用。用于修饰它支持的程序元素

示例一:生成文档相关的注解

@author 标明开发该类模块的作者,多个作者之间使用”,”分割

@version 标明该类模块的版本

@see 参考转向,也就是相关主题

@since哪个版本开始增加

@param 对方法中某参数的说明,如果没有参数就不能写

@return方法返回值的说明,如果方法的返回值类型是void就不能写

@exception 对方法可能抛出的异常进行说明,如果方法没有用throws显式抛出的异常就不能写

其中@param、@return和@exception这三个标记都是只用于方法

@param的格式要求:@param 形参名 形参类型 形参说明

@return的格式要求:@return 返回值类型 返回值说明

@exception的格式要求:@exception 异常类型 异常说明

@param和@exception可以并列多个

示例二:在编译时进行格式检查(JDK内置的三个基本注解)

@Override 限定重写父类方法,该注解只能用于方法

@Deprecated 用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择

@suppressWarnings 抑制编译器警告

示例三:跟踪代码依赖性,实现替代配置文件功能

@WebServlet Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署

@Transactional spring框架中关于“事务”的管理

示例四:Junit单元测试中的注解

Junit单元测试中也有大量注解的使用。简单罗列到下面,这里不再赘述
@Test 标记在非静态的测试方法上。只有标记@Test的方法才能被作为一个测试方法单独测试一个类中可以有多个@Test标记的方法,运行时如果只想运行共中一个@Test标记的方法,那么选择这个方法名,然后单独运行,否则整个类的所有标记了@Test的方法都会被执行

  • @Test(timeout=1000):设置超时时间,如果测试时间超过了你定义的timeout,测试失败
  • @Test(expected):申明出会发生的异常,比如@Test ( expected = Exception.class )

了解:

@BeforeClass 标记在静态方法上。因为这个方法只执行一次。在类初始化时执行

@AfterClass 标记在静态方法上。因为这个方法只执行一次。在所有方法完成后执行

@Before 标记在非静态方法上。在@Test方法前面执行,而且是在每一个@Test方法前面都执行

@After 标记在非静态方法上。在@Test方法后面执行,而且是在每一个@Test方法后面都执行

@Ignore 标记在本次不参与测试的方法上。这个注解的含义就是”某些方法尚未完成,暂不参与此次测试

@BeforeClass、@AfterClass、@Before、@After、@Ignore都是配合@Test它使用的,单独使用没有意义

9.2.2 自定义注解

①注解声明为@interface,并且都会指明两个元注解(Retention、Target)

②自定义注解自动继承了java.lang.annotation.Annotation接口

③Annotation的成员变量在 Annotation定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型以上所有类型的数组

④可以在定义 Annotation的成员变量时为其指定初始值,指定成员变量的初始值可使用default关键字

⑤如果只有一个参数成员,建议使用参数名为value

⑥如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式是“参数名=参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”

没有成员定义的Annotation称为标记;包含成员变量的 Annotation称为元数据Annotation

注意:自定义注解必须配上注解的信息处理流程才有意义

public @interface MyAnnotation {
    //自定义注解:以@suppressWarnings为例
    String value() default "hello";
}
@MyAnnotation(value = "hi")
class Test{

}

9.2.3 JDK中的元注解

JDK的元Annotation用于修饰其他Annotation定义,即对现有的注解进行解释说明的注解

JDK5.0提供了4个标准的meta-annotation类型, 分别是:

  • Retention:指定该Annotation的生命周期,只有声明为RUNTIME的注解才能通过反射获取
  • Target:指定被修饰的Annotation能用于修饰哪些程序元素
  • Documented:指定被该元Annotation修饰的Annotation类将被javadoc工具提取成文档
  • Inherited:被它修饰的Annotation将具有继承性
1.Retention

@Retention:只能用于修饰一个Annotation定义,用于指定该Annotation的生命周期,@Rentention包含一个RetentionPolicy类型的成员变量,使用@Rentention时必须为该value成员变量指定值:

  • RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释
  • RetentionPolicy.CLASS:在class文件中有效(即class保留),当运行 Java程序时,JVM不会保留注解。这是默认值
  • RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java程序时, JVM会保留注释。程序可以通过反射获取该注释
2.Target

@Target:用于修饰Annotation定义,用于指定被修饰的Annotation能用于修饰哪些程序元素

@Target也包含一个名为value的成员变量:

  • CONSTRUCTOR :用于描述构造器
  • FIELD:用于描述域
  • LOCAL_VARIABLE:用于描述局部变量
  • METHOD:用于描述方法
  • PACKAGE:用于描述包
  • PARAMETER:用于描述参数
  • TYPE:用于描述类、接口(包括注解类型)或enum声明
3.Documented

@Documented:用于指定被该元Annotation修饰的Annotation类将被javadoc工具提取成文档。默认情况下,javadoc 是不包括注解的

  • 定义为Documented的注解必须设置Retention值为RUNTIME
4.Inherited

@Inherited:被它修饰的Annotation将具有继承性。如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解

  • 比如:如果把标有@Inherited注解的自定义注解标注在类级别上,子类则可以继承父类类级别的注解
  • 实际应用中,使用较少

9.2.4 JDK8中注解的新特性

1.可重复注解
//JDK8及以后:
//①在MyAnnotation上声明@Repeatable,成员值为MyAnnotations.class
//②MyAnotation的Target和Retention要与MyAnnotations相同
@Repeatable(value = MyAnnotations.class)
@Target({ElementType.TYPE,ElementType.PARAMETER,ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
@Target({ElementType.TYPE,ElementType.PARAMETER,ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
pubilc @interface MyAnnotations {
    MyAnnotation[] value();
}
//可重复注解JDK8之前的写法:新建一个容器注解来保存这多个相同类型的注解
//@MyAnnotations({@MyAnnotation(value = "hi"),@MyAnnotation(value = "hello")})
@MyAnnotation(value = "hello")
@MyAnnotation(value = "hi")
public class AnnotationTest{
}
2.类型注解

JDK1.8之后,关于元注解@Target的参数类型ElementType枚举值多了两个:TYPE_ PARAMETER,TYPE_ USE

在Java8之前,注解只能是在声明的地方所使用,Java8开始,注解可以应用在任何地方

  • ElementType.TYPE_PARAMETER表示该注解能写在类型变量的声明语句中(如:泛型声明)
  • ElementType.TYPE_USE表示该注解能写在使用类型的任何语句中
@Target({ElementType.TYPE_PARAMETER,ElementType.TYPE_USE})
public @interface MyAnnotation1 {
}

class Generic<@MyAnnotation1 T>{
    public void show(){
        ArrayList<@MyAnnotation1 String> list = new ArrayList<>();
        int num = (@MyAnnotation1 int)10L;
    }
}

十、Java集合

10.1 Java结合框架概述

一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊端,而Java 集合可以动态地把多个对象的引用放入容器中

集合、数组都是对多个数据进行存储操作的结构,简称Java容器(此时的存储主要指内存层面的存储,不涉及到持久化的存储)

  • 数组在内存存储方面的特点:
    • 数组初始化以后,长度就确定了(长度确定
    • 数组声明的类型,就决定了进行元素初始化时的类型(元素类型确定
  • 数组在存储数据方面的弊端:
    • 数组初始化以后,长度就不可变了,不便于扩展
    • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数
    • 数组存储的数据是有序的、可以重复的。—>存储数据的特点单一

Java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组

10.1.1 集合框架的使用场景

10.1.2 Java集合可分为Collection和Map两种体系

  • Collection接口:单列数据,定义了存取一组对象的方法的集合
    • List:元素有序、可重复的集合 —>”动态”数组
      • 实现类:ArrayList、LinkedList、Vector
    • Set:元素无序、不可重复的集合 —>高中学的”集合”
      • 实现类:HashSet、LinkedHashSet、TreeSet
  • Map接口:双列数据,保存具有映射关系“key-value对”的集合 —>高中函数:y=f(x),x相当于key,y相当于value,可以有多个key指向同一个value(key->value多对一),不能有一个key指向多个value(value->key一对一
    • 实现类:HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

1.Collection接口继承树:

2.Map接口继承树:

10.2 Collection接口

10.2.1 Collection接口中常用方法的使用

添加元素:add(Object obj)、addAlI(Collection coll)

获取有效元素的个数:int size()

清空集合:void clear()

是否是空集合:boolean isEmpty()

Collection collection = new ArrayList();
//1.add(Object e):将元素e添加到集合collection中
collection.add("AA");
collection.add("AA");
collection.add(123);//自动装箱
collection.add(new Date());
//2.size():获取添加的元素个数
System.out.println(collection.size());//4
//3.addAll(Collection collection):将collection集合中的元素添加到当前集合中
Collection collection1 = new ArrayList();
collection1.add(456);
collection1.add("CC");
collection.addAll(collection1);
System.out.println(collection.size());//6
System.out.println(collection);
//4.clear():清空集合元素
collection.clear();//对象依然存在,只是元素被清空了
//5.isEmpty():判断当前集合是否为空
System.out.println(collection.isEmpty());

是否包含某个元素

  • boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
  • boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较
//6.contains(Object obj):判断当前集合是否包含obj
//在判断时会调用obj对象所在类的equals()方法,如果所在类没有重写equals(),则会调用Object类的equals()比较两个对象的引用地址,所以,如果我们要比较两个对象的内容,必须重写equals()
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(false);
collection.add(new String("Jerry"));
collection.add(new Person("Tom",20));
//Person p = new Person("Tom", 20);
//collection.add(p);
//System.out.println(collection.contains(p));//true
boolean contains = collection.contains(123);
System.out.println(contains);//true
System.out.println(collection.contains(new String("Jerry")));//true,因为String类重写了equals方法,所以这里比较的是内容
System.out.println(collection.contains(new Person("Tom", 20)));//false,因为我们的自定义类Person没有重写equals方法,所以是相当于用“==”比较的地址值

//7.containsAll(Collection collection):判断形参collection中的所有元素是否都存在于当前集合中
Collection collection1 = Arrays.asList(123,456);//返回一个List,也是一种创建集合的方式
System.out.println(collection.containsAll(collection1));//true

删除元素:

  • boolean remove(Object obj):通过元素所属类的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
  • boolean removeAll(Collection coll):取当前集合的差集
 //8.remove(Object obj):从当前集合移除obj元素,返回true、false,移除元素后的集合仍保存在当前集合
 Collection collection = new ArrayList();
 collection.add(123);
 collection.add(456);
 collection.add(false);
 collection.add(new String("Jerry"));
 collection.add(new Person("Tom",20));
boolean remove = collection.remove(123);
System.out.println(remove);//true
System.out.println(collection);//[456, false, Jerry, Collection.Person@4ee285c6]

//9.removeAll(Collection collection):从当前集合中移除collection中的所有元素
Collection collection1 = Arrays.asList(123,456);
collection.removeAll(collection1);
System.out.println(collection);//[false, Jerry, Collection.Person@4ee285c6]

取两个集合的交集:boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c

集合是否相等:boolean equals(Object obj):如果两个List(有序)集合中的元素内容一样,顺序不一样,则会返回false(List的情况);如果是Set(无序)集合,顺序不一样内容一样仍为true

//10.retainAll(Collection collection):获取当前集合和collection的交集,并将交集保留在当前集合
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(false);
collection.add(new String("Jerry"));
collection.add(new Person("Tom",20));
Collection collection1 = Arrays.asList(123,456);
collection.retainAll(collection1);
System.out.println(collection);//[123, 456]
System.out.println(collection1);//[123, 456]

//11.equals():判断当前集合与collection是否相等(比较内容)
System.out.println(collection.equals(collection1));//true,因为上方用retainAll取了这两个集合的交集。保存在collection中,而collection1不变,所以这里比较就返回true

转成对象数组:Object[] toArray()

获取集合对象的哈希值:hashCode()

Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(false);
collection.add(new String("Jerry"));
collection.add(new Person("Tom",20));

//12.HashCode():返回当前对象的Hash值
System.out.println(collection.hashCode());

//13.toArray():集合  -->  数组
Object[] objects = collection.toArray();
for (int i = 0; i < objects.length; i++) {
System.out.println(objects[i]);
}
//扩展:数组  -->  集合:调用Arrays类的静态方法asList()
List<String> strings = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(strings);
List<int[]> ints = Arrays.asList(new int[]{123, 456});
System.out.println(ints.size());//1,因为int是一个基本数据类型,集合中只能是引用类型,如果用基本数据类型这儿会把它认为是一个元素,而下方使用包装类数据建立的集合就是两个元素
List<Integer> integers = Arrays.asList(new Integer[]{123, 456});
System.out.println(integers.size());//2

遍历:iterator():返回迭代器对象,用于集合遍历,内部方法:hasNext(),next(),remove()

Iterator对象称为迭代器(设计模式的一种),主要用于遍历Collection集合中的元素

GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生

Collection接口继承了java.lang.lterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法, 用以返回一个实现了iterator接口的对象

Iterator仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建terator对象,则必须有一个被迭代的集合

集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前

//集合元素的遍历操作,使用迭代器Iterator接口
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(false);
collection.add(new String("Jerry"));
collection.add(new Person("Tom",20));
Iterator iterator = collection.iterator();
//遍历方式一:
//System.out.println(iterator.next());
//System.out.println(iterator.next());
//System.out.println(iterator.next());
//System.out.println(iterator.next());
//System.out.println(iterator.next());
//System.out.println(iterator.next());//超出集合元素个数范围,抛出NoSuchElementException异常
//遍历方式二:不推荐
for (int i = 0; i < collection.size(); i++) {
    System.out.println(iterator.next());
}
//遍历方式三:推荐
while(iterator.hasNext()) {
    System.out.println(iterator.next());
}

//错误写法一:
Iterator iterator = collection.iterator();
while(iterator.next() != null) {
    System.out.println(iterator.next());//隔空遍历输出,判断时迭代器指针往下移,输出时又往下移,所以造成隔空输出
}
//错误写法二:
while(collection.iterator().hasNext()){
    System.out.println(collection.iterator().next());//每次都是输出123,因为每次调用iterator()都会返回一个新的迭代器对象,所以每次都是在遍历第一个元素
}

//迭代器移除元素:remove()
while(iterator.hasNext()){
    Object obj = iterator.next();
    if(obj.equals("Jerry")){
         iterator.remove();   
    }
}
//注意:Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法
//如果还未调用next()或在上一次调用next方法之后已经调用了remove方法,再调用remove都会报IllgalStateException

JDK5新增了一个增强for循环foreach,用于遍历集合和数组

Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(false);
collection.add(new String("Jerry"));
collection.add(new Person("Tom",20));
//集合中元素的类型  局部变量 : 需要遍历的集合或数组对象
//内部仍是使用迭代器
for(Object obj : collection){
    System.out.println(obj);
}

10.2.2 Collection的子接口List

鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组

List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引

List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据字号存取容器中的元素

JDK API中List接口的实现类常用的有: ArrayList、 LinkedList和Vector(list的古老实现类)

面试题:ArrayList、LinkedList、Vector三者的异同?
同:三个类都实现List接口,存储数据的特点:存储有序、可重复的数据;ArrayList和Vector底层使用Object[]存储,LinkedList使用双向链表
异:
|--ArrayList:作为list接口的主要实现类,使用最多;线程不安全,效率高;对于数据的查询效率高,但频繁的插入和删除效率低;底层数组扩容一次为1.5倍
|--LinkedList:线程不安全,效率高;对于频繁的插入、删除操作效率高,但查询效率低;底层使用双向链表存储
|--Vector:作为list接口的古老实现类;线程安全,效率低;底层数组扩容一次2倍
1.ArrayList类

ArrayList是List接口的典型实现类、主要实现类

本质上,ArrayList是对象引用的一个”变长”数组

ArrayList的JDK1.8之前与之后的实现区别?

  • JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
  • JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组

Arrays.asList(…)方法返回的List集合,既不是ArrayList实例,也不是Vector 实例。Arrays.asList(…)返回值是一个固定长度的List集合

源码分析:JDK7
底层存储数据:Object[] elementData
ArrayList list = new ArrayList();//底层创建一个初始长度为10的Object数组
//无参构造器源码
public ArrayList() {
    this(10);//调用有参构造器,传入容量值为10,直接创建this.elementData = new Object[10];
}
//add()方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 判断是否超出list范围,若超出则会调用扩容的方法grow,grow的内容可参考下方JDK8中的grow方法
    elementData[size++] = e;//将数据e添加到扩容后的list
    return true;
}
List.add(123);//elementData[0] = new Integer(123);
...
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中
结论:建议开发中使用带参的构造器: ArrayList list = new ArrayList(int capacity)
源码分析:JDK8中ArrayList的变化
底层存储数据:仍为Object[] elementData
ArrayList list = new ArrayList();//底层object[] eLementData初始化为{},并没有创建长度为10的Object数组
//无参构造器源码
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//DEFAULTCAPACITY_EMPTY_ELEMENTDATA={}
}
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData
...
后续的添加和扩容操作与jdk7无异
//add()方法添加数据及调用的一系列方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 判断是否超出list范围
    elementData[size++] = e;//将数据e添加到扩容后的list
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));//将calculateCapacity返回的容量值传入
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//判断添加的这个数据是否是第一个数据
        return Math.max(DEFAULT_CAPACITY, minCapacity);//如果是则返回Object数组默认容量10和添加这个数据后的数组中需要的容量中更大的那个值,第一次添加其实返回的就是默认容量10
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {//传入需要的容量值,第一次添加传入的是10
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)//传入的需要容量的值是否比数组elementData现有长度大
        grow(minCapacity);//如果是,则进行扩容
}
private void grow(int minCapacity) {//传入需要用到的容量10
    // overflow-conscious code
    int oldCapacity = elementData.length;//第一次添加数据,length为0
    int newCapacity = oldCapacity + (oldCapacity >> 1);//扩容1.5倍
    if (newCapacity - minCapacity < 0)//若扩容后的容量值还是需要的小
        newCapacity = minCapacity;//则把需要的容量值赋给这个新容量来进行扩容
    if (newCapacity - MAX_ARRAY_SIZE > 0)//若扩容后这个需要用到的容量值比Integer.MAX_VALUE-8还大,则调用hugeCapacity()
        newCapacity = hugeCapacity(minCapacity);
    //扩容后将原数组的值复制到扩容过后的数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式延迟了数组的创建,节省内存

2.LinkedList类

对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高

新增方法:

  • void addFirst(Object obj)
  • void addLast(Object obj)
  • Object getFirst()
  • Object getLast()
  • Object removeFirst()
  • Object removeLast()
源码分析
底层存储数据:双向链表
LinkedList linkedList = new LinkedList();//内部声明了Node类型的first和last属性,默认值为null
last.add(123);//将123封装在Node中,创建了Node对象
其中,Node定义为:
private static class Node<E> {
    E item;//要添加的数据本身
    Node<E> next;//该结点需要指向的下一个结点的地址值
    Node<E> prev;//该结点需要指向的上一个结点的地址值

    Node(Node<E> prev, E element, Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
    }
}
添加数据时调用的`add()`方法中调用的方法(尾插):
void linkLast(E e) {
    final Node<E> l = last;//将l和last指向同一个结点
    final Node<E> newNode = new Node<>(l, e, null);//创建一个新结点,保存新数据,再把新结点的prev指向添加新结点前的最后一个结点
    last = newNode;//将新结点作为该链表的最后一个结点
    if (l == null)//判断新添加的结点是否是这个链表的第一个结点
        first = newNode;//如果是,就让first指向新结点
    else
        l.next = newNode;//如果不是,则让添加新结点前的最后一个结点的next指向新结点
    size++;//链表数据数量加1
    modCount++;
}
3.Vector类
源码分析
jdk7和jdk8中通过`Vector()`构造器创建对象时,底层都创建了长度为10的数在扩容方面,默认扩容为原来的数组长度的2倍
该类已经不用了,尽管线程安全,但是现有Collections工具类也能把ArrayList变成线程安全的,所以仍用ArrayList替代Vector
4.List接口中的常用方法

List除了从Collection集合继承的方法外,List集合里添加了一些根据索引来操作集合元素的方法

  • void add(int index,Object ele):在index位置插入ele元素
  • boolean addAll(int index,Collection eles):从index位置开始将eles中的所有元素添加进来
  • Object get(int index):获取指定index位置的元素
  • int indexOf(Object obj):返回obj在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
  • Object remove(int index):移除指定index位置的元素,并返回此元素
  • Object set(int index,Object ele):设置指定index位置的元素为ele
  • List subList(int fromIndex,int tolndex):返回从fromIndex到tolndex位置的子集合
ArrayList arrayList = new ArrayList();
arrayList.add(123);
arrayList.add(456);
arrayList.add("AA");
arrayList.add(new Person("Tom",20));
arrayList.add(456);
System.out.println(arrayList);//[123, 456, AA, Collection.Person@4ee285c6, 456]
//1.void add(int index,Object ele):在index位置插入ele元素
arrayList.add(1,"BB");
System.out.println(arrayList);//[123, BB, 456, AA, Collection.Person@4ee285c6, 456]
//2.boolean addAll(int index,Collection eles):从index位置开始将eles中的所有元素添加进来
List list = Arrays.asList(1,2,3);
arrayList.addAll(1,list);
System.out.println(arrayList);//[123, 1, 2, 3, BB, 456, AA, Collection.Person@4ee285c6, 456]
//3.Object get(int index):获取指定index位置的元素
System.out.println(arrayList.get(4));//BB
//4.int indexOf(Object obj):返回obj在集合中首次出现的位置
System.out.println(arrayList.indexOf(456));//5
//5.int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
System.out.println(arrayList.lastIndexOf(456));//8
//6.Object remove(int index):移除指定index位置的元素,并返回此元素
Object remove = arrayList.remove(8);
System.out.println(arrayList);//[123, 1, 2, 3, BB, 456, AA, Collection.Person@4ee285c6]
System.out.println(remove);//456
//7.Object set(int index,Object ele):设置指定index位置的元素为ele
arrayList.set(5,4567);
System.out.println(arrayList);//[123, 1, 2, 3, BB, 4567, AA, Collection.Person@4ee285c6]
//8.List subList(int fromIndex,int tolndex):返回从fromIndex到tolndex位置的左闭右开区间的子集合
List list1 = arrayList.subList(3,6);
System.out.println(list1);//[3, BB, 4567]
System.out.println(arrayList);//[123, 1, 2, 3, BB, 4567, AA, Collection.Person@4ee285c6],原list没变
5.List的一道面试笔试题
@Test
public void test1() {
    //区分List中remove(int index)和remove(0bject obj)
    List list = new ArrayList();
    list.add(1);
    list.add(2);
    list.add(3);
    updateList(list);
    System.out.println(list);
}
private void updateList(List list){
    //list.remove(2);//移除索引为2的元素
    list.remove(new Integer(2));//移除值为2的元素
}

10.2.3 Collection子接口Set

Set接口是Collection的子接口,set接口没有提供额外的方法(即Set中使用的都是Collection接口中声明过的方法)

Set集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set集合中,则添加操作失败

(HashSet和LinkedHashSet)判断两个对象是否相同不是使用==运算符,而是根据equals()方法和hashCode()方法,所以添加的数据所在类一定要重写equals()和hashCode()方法而(TreeSet)判断对象是否相同通过Comparable接口和Comparator接口

|--Set接口:存储无序的、不可重复的数据--> 高中讲的“集合”
    |--HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
        |--LinkedHashSet:作为HashSet的子类; 遍历其内部数据时,可以按照添加的顺序遍历;
            在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据
            对于频繁的遍历操作,LinkedHashSet 效率高于HashSet
    |--TreeSet:可以按照添加对象的指定属性,进行排序

Set的无序性和不重复性的理解

/*
    Set:存储无序、不可重复的数据
    以HashSet为例说明:
    1、无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序进行添加,而是根据数据的Hash值确定
    2、不可重复性:保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个
*/
Set set = new HashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom",20));
set.add(new User("Tom",20));
set.add(129);
Iterator iterator = set.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());//AA CC 129 User{name='Tom', age=20} 456 123
}
1.HashSet类

HashSet是Set接口的典型实现,大多数时候使用Set集合时都使用这个实现类

HashSet按Hash算法来存储集合中的元素,因此具有很好的存取、查找、删除性能

HashSet具有以下特点:

  • 不能保证元素的排列顺序
  • HashSet不是线程安全的
  • 集合元素可以是null

HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相等

对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“ 相等的对象必须具有相等的散列码

重写HashCode()方法的基本原则:

  • 在程序运行时,同一个对象多次调用hashCode()方法应该返回相同的值
  • 当两个对象的equals()方法比较返回true时,这两个对象的hashCode()方法的返回值也应相等
  • 对象中用作equals()方法比较的Field(属性), 都应该用来计算hashCode值

HashSet中元素的添加过程:

HashSet底层:数组+链表
1.我们向HashSet中添加元素a,首先调用元素a所在类的`hashCode()`方法,计算元素a的哈希值,通过此哈希值再由某种算法计算出元素a应该在HashSet底层数组中的存放位置(即为:索引位置)
2.判断数组此位置上是否已经有元素:
    (1)如果此位置上没有其他元素,则元素a直接添加到底层数组的此位置上  -->添加成功的情况1
    (2)如果此位置上已有其他元素b(或以链表形式存在了多个元素),则比较元素a与元素b(或这些元素)的hash值:
        ①如果hash值不相同,则元素a添加成功  -->添加成功的情况2
        ②如果hash值相同,进而需要调用元素a所在类的equals()方法:
            equals()返回true,元素a添加失败
            equals()返回false,则元素a添加成功  -->添加成功的情况3
3.说明:对于添加成功的情况2和情况3而言:元素a与已经存在在指定索引位置上的数据以链表的方式存储
    jdk7:元素a放到数组中,指向原来的元素
    jdk8:原来的元素在数组中,指向元素d
总结:七上八下(JDK7:新元素以头插法放在所有链表数据前面,即数组中;JDK8:新元素以尾插法放在链表数据后面,即链表尾部)
2.LinkedHashSet类

LinkedHashSet是HashSet的子类

LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的

LinkedHashSet插入性能略低于HashSet, 但在迭代访问Set里的全部元素时有很好的性能

LinkedHashSet不允许集合元素重复

//LinkedHashSet的使用
Set set = new LinkedHashSet();
set.add(456);
set.add(new String("AA"));
set.add(456);
set.add(new User("Tom",30));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());//456 AA User{name='Tom', age=30}
}
3.TreeSet类

TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态

TreeSet两种排序方法:自然排序(Comparable)和定制排序(Comparator)默认情况下,TreeSet采用自然排序

TreeSet底层使用红黑树结构存储数据,向TreeSet中添加的数据要求必须是相同类的对象

新增的方法如下:(了解)

  • Comparator comparator()
  • Object first()
  • Object last()
  • Object lower(Object e)
  • Object higher(Object e)
  • SortedSet subSet(fromElement, toElement)
  • SortedSet headSet(toElement)
  • SortedSet tailSet(fromElement)

TreeSet默认情况下添加数据:

默认情况(自然排序)下,TreeSet调用add()添加数据时会调用该数据对象的所属类中的compareTo()方法对这些对象数据进行强制排序,所以向TreeSet中添加的数据其所属类必须要实现Comparable接口,重写compareTo()方法,并在其中指明该类型的对象以哪种方法进行排序
自然排序中,比较两个对象是否相同的标准为: compareTo()返回值为0而不再是equals()和hashCode()的比较结果
TreeSet set = new TreeSet();
//添加失败,TreeSet中不能添加不同类的元素
set.add(456);
set.add(new String("AA"));
set.add(456);
set.add(new User("Tom",30));
//添加成功,举例一:Integer包装类实现了Comparable接口并重写了compareTo()方法,所以添加成功后数据顺序为-34、34、44、65
set.add(34);
set.add(-34);
set.add(44);
set.add(65);
//添加成功,举例二:
set.add(new User("Tom",10));
set.add(new User("Jack",50));
set.add(new User("Jerry",20));
set.add(new User("Eric",25));
/*添加成功后数据顺序为:
User{name='Tom', age=10}
User{name='Jerry', age=20}
User{name='Jack', age=50} 
User{name='Eric', age=25}
*/
//User类重写的compareTo方法
@Override
public int compareTo(Object o) {
    //先按姓名从大到小排,再按年龄从小到大排
    if(o instanceof User){
        User user = (User) o;
        //return -this.name.compareTo(user.name);
        int compare = -this.name.compareTo(user.name);
        if(compare != 0){
            return compare;
        }else{
            return Integer.compare(this.age,user.age);
        }
    }else{
        throw new RuntimeException("输入的类型不匹配");
    }
}

TreeSet定制排序下添加数据:

定制排序情况下,TreeSet调用add()添加数据时会调用`new TreeSet(comparator)`中传入的Comparator实现类对象重写的compare()方法对这些对象数据进行对应的强制排序
定制排序中,比较两个对象是否相同的标准为: compare()返回值为0而不再是equals()和hashCode()的比较结果
Comparator comparator = new Comparator() {
    //只考虑年龄的情况下从小到大排序
    @Override
    public int compare(Object o1, Object o2) {
        if(o1 instanceof User && o2 instanceof User){
        User user1 = (User) o1;
        User user2 = (User) o2;
        return Integer.compare(user1.getAge(),user2.getAge());
        }
        throw new RuntimeException("比较的类型不正确");
    }
};
TreeSet set = new TreeSet(comparator);
set.add(new User("Tom",10));
set.add(new User("Jack",50));
set.add(new User("Jerry",20));
set.add(new User("Mary",20));//年龄相等,但是由于Jerry先被添加到TreeSet中,所以Mary进不能再被添加到其中
set.add(new User("Eric",25));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
/*
添加数据后的顺序为:
User{name='Tom', age=10}
User{name='Jerry', age=20}
User{name='Eric', age=25}
User{name='Jack', age=50}
*/
4.Set两道相关面试题
//练习:在List内去除重复数字值,要求尽量简单
public static List duplicateList(List list) {
    HashSet set = new HashSet( ) ;
    set.addA1l(list);
    return new ArrayList(set);
}
public static void main(String[] args){
    List list = new ArrayList();
    list.add(new Integer(1));
    list.add(new Integer(2));
    list.add(new Integer(2));
    list.add(new Integer(4));
    list.add(new Integer(4));
    List list2 = duplicatelist(list);
    for (bject integer : list2) {
        System.out.println(integer);
    }
}
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA"); 
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
System.out.println(set);//1002,"BB"  1001,"AA" //根据重写后的hashCode()计算出对应存储位置添加到set中后

p1.name = "CC";//修改p1的name属性,此时set中的p1变为1001,"CC",但其存储的地址是由1001,"AA"经过重写后的hashCode()和equals()得到的
set.remove(p1); //此时的p1为1001,"CC",则会使用此时的1001,"CC"通过重写后的hashCode()计算得到其在set中对应的位置,显然计算出的位置是不同的,所以移除1001,"CC"失败
System.out.println(set);//1002,"BB"  1001,"CC" 此时"CC"是"AA"被修改后的数据

set.add(new Person(1001,"CC"));//能添加成功,因为能由上方解释得出1001,"CC"和1001,"AA"经由重写后的hashCode()计算出的存储位置是不同的
System.out.println(set);//1002,"BB"  1001,"CC"  1001,"CC"

set.add(new Person(1001,"AA"));//能添加成功,虽然此时新添加的这个1001,"AA"经由重写后的hashCode()计算出的位置和p1相同,但此时会通过equals()判断他们的内容是不是都相等,显然"AA"不等于"CC",所以这个数据会以链表的形式与p1存储在一起
System.out.println(set);//1002,"BB"  1001,"CC"  1001,"CC"  1001,"AA"
//其中Person类中写了hashCode()和equal()方法

10.3 Map接口

|--Map:双列数据,存储key-value对的数据   ---类似于高中的函数: y = f(x)
    |--HashMap:作为Map的主要实现类,使用最多;线程不安全,效率高;能存储null的key-value对;
        HashMap的底层:数组+链表(jdk7及之前)   数组+链表+红黑树(jdk8)
        |--LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历
            原因:在原有的HashMap底层结构基础上添加了一对指针,指向前一个和后一个元素
            对于频繁的遍历操作,此类执行效率高于HashMap
    |-- TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
            底层采用红黑树结构存储数据
    |--Hashtable:作为Map的古老实现类;线程安全,效率低;不能存储null的key-value对
        |--Properties:常用来处理配置文件。key和value都是String类型
        
        
面试题:
1. HashMap的底层实现原理?
2. HashMap和Hashtable的异同?
3. CurrentHashMap与Hashtable的异同? (暂时不讲)
4. 谈谈你对HashMap中put/get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?

Map结构的理解:

Map中的key:无序的、不可重复的,使用Set 存储所有的key —> key所在的类要重写equals()和hashCode() (以HashMap为例),因为会利用这两个方法决定key的存储位置

Map中的value:无序的、可重复的,使用Collection存储所有的value —>value所在的类要重写equals(),因为以后可能会涉及到value的比较

一个键值对:key-value构成了一个Entry对象,key和value相当于Entry的两个属性

Map中的entry:无序的、不可重复的,使用Set存储所有的entry

10.3.1 Map接口中的常用方法

添加、删除、修改操作:

  • Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象
  • void putAll(Map m):将m中的所有key-value对存放到当前map
  • Object remove(Object key):移除指定key的key-value对,并返回value
  • void clear():清空当前map中的所有数据
Map map = new HashMap();
//Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
map.put("AA",123);
map.put(45,123);
map.put("BB",56);
//修改
map.put("AA",87);
System.out.println(map);//{AA=87, BB=56, 45=123}
//void putAll(Map m):将m中的所有key-value对存放到当前map中
Map map1 = new HashMap();
map1.put("CC",123);
map1.put("DD",432);
map.putAll(map1);
System.out.println(map);//{AA=87, BB=56, CC=123, DD=432, 45=123}
//Object remove(Object key):移除指定key的key-value对,并返回value
System.out.println(map.remove("AA"));//87
System.out.println(map);//{BB=56, CC=123, DD=432, 45=123}
//void clear():清空当前map中的所有数据
map.clear();//与map=null不同,只是清除map中的元素,map对象还在
System.out.println(map.size());//0
System.out.println(map);//{}

元素查询的操作:

  • Object get(Object key):获取指定key对应的value
  • boolean containsKey(Object key):是否包含指定的key
  • boolean containsValue(Object value):是否包含指定的value
  • int size():返回map中key-value对的个数
  • boolean isEmpty():判断当前map是否为空
  • boolean equals(Object obj):判断当前map和参数对象obj是否相等
Map map = new HashMap();
map.put("AA",123);
map.put(45,123);
map.put("BB",56);
//Object get(Object key):获取指定key对应的value
System.out.println(map.get(45));//123
System.out.println(map.get(456));//null(不存在)
//boolean containsKey(Object key):是否包含指定的key
System.out.println(map.containsKey("AA"));//true
System.out.println(map.containsKey("CC"));//false
//boolean containsValue(Object value):是否包含指定的value
System.out.println(map.containsValue(123));//true
System.out.println(map.containsValue(456));//false
//int size():返回map中key-value对的个数
System.out.println(map.size());//3
//boolean isEmpty():判断当前map是否为空
System.out.println(map.isEmpty());//false
map.clear();
System.out.println(map.isEmpty());//true
//boolean equals(Object obj):判断当前map和参数对象obj是否相等
Map map1 = new HashMap();
map1.put("AA",123);
map1.put(45,123);
//map1.put("BB",56);
System.out.println(map.equals(map1));//false
//System.out.println(map.equals(map1));//true

元视图操作的方法(遍历key-value对):

  • Set keySet():返回所有key构成的Set集合
  • Collection values():返回所有value构成的Collection集合
  • Set entrySet():返回所有key-value对构成的Set集合
Map map = new HashMap();
map.put("AA",123);
map.put(45,1234);
map.put("BB",56);
//Set keySet():返回所有key构成的Set集合
Set set = map.keySet();
Iterator iterator = set.iterator();
while(iterator.hasNext()) {
    System.out.println(iterator.next());//AA  BB  45
}
//Collection values():返回所有value构成的Collection集合
Collection values = map.values();
for (Object value : values) {
    System.out.println(value);//123  56  1234
}
//Set entrySet():返回所有key-value对构成的Set集合
Set set1 = map.entrySet();
Iterator iterator1 = set1.iterator();
while(iterator1.hasNext()) {
    Object obj = iterator1.next();
    Map.Entry entry = (Map.Entry) obj;
    System.out.println(entry);
    System.out.print(entry.getKey() + "  -->  ");
    System.out.println(entry.getValue());
    //AA=123
    //AA  -->  123
    //BB=56
    //BB  -->  56
    //45=1234
    //45  -->  1234
    //System.out.println(iterator1.next());
}

10.3.2 HashMap类

1.底层实现原理

JDK7

HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table
...可能已经执行过多次put后...
map.put(key1, value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置
    如果此位置上的数据为空,此时的entry1(key1-value1)添加成功  ----添加成功情况1
    如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据
    的哈希值:
        如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功  ----添加成功情况2
        如果key1的哈希值和已经存在的某一个数据(key2-value2) 的哈希值相同,继续比较:调用key1所在类的equals(key2)
            如果equals()返回false:此时key1-value1添加成功  ----添加成功情况3
            如果equals()返回true:使用value1替换value2
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储(JDK7:新元素以头插法放在所有链表数据前面,即数组中;JDK8:新元素以尾插法放在链表数据后面,即链表尾部)
另外,在不断的添加过程中,会涉及到扩容问题,默认的打容方式:扩容为原来容量的2倍,并将原有的数据复制过来

JDK8

jdk8相较于jdk7在底层实现方面的不同:
1. new HashMap():底层没有创建一个长度为16的数组,当首次调用put()方法时,底层才创建长度为16的数组
2. jdk8底层的数组是: Node[],而非Entry[]
3. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储
2.源码分析

HashMap源码中的重要常量:

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
  • MAXIMUM_CAPACITY : HashMap的最大支持容量, 2^30
  • DEFAULT_LOAD_FACTOR : HashMap的默认加载因子
  • TREEIFY_THRESHOLD : Bucket中链表长度大于该默认值,转化为红黑树
  • UNTREEIFY_THRESHOLD : Bucket中红黑树存储的Node小于该默认值,转化为链表
  • MIN_TREEIFY_CAPACITY : 桶中的Node被树化时最小的hash表容量。( 当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作,这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_ _THRESHOLD的4倍)
  • table : 存储元素的数组,总是2的n次幂
  • entrySet : 存储具体元素的集
  • size : HashMap中存储的键值对的数量
  • modCount : HashMap扩容和结构改变的次数
  • threshold : 扩容的临界值,= 容量 * 填充因子
  • loadFactor : 填充因子
面试题:负载因子值的大小,对HashMap有什么影响
1.负载因子的大小决定了HashMap的数据密度
2.负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降
3.负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间
4.按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数

JDK7

//无参构造器
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);//传入16  0.75到有参构造器
}
//有参构造器
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)//判断传入的初始化容量是否小于0
        throw new IllegalArgumentException("Illegal initial capacity:" + initialCapacity);
    if (initialCapacity > MAXIMUM_ CAPACITY)//判断传入的初始化容量是否大于HashMap中预先定义的最大容量
        initialCapacity = MAXIMUM_ CAPACITY;
    if (1oadFactor <= 0 || Float.isNaN(loadFactor))//判断填充因子是否小于等于0或是否为负数
        throw new IllegalArgumentException("Illegal load factor:" + loadFactor);
    // Find a power of 2 >= initialCapacity
    int capacity = 1;//定义一个容量变量
    while (capacity < initialCapacity)//如果容量值比给定的初始化容量小,则扩大为原来的2倍
        capacity <<= 1;
    this.1oadFactor = loadFactor;//将传入的填充因子赋值给这个新定义的HashMap对象(或是说Entry数组)
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//设置扩容临界值,默认16*0.75=12
    table = new Entry[capacity];//定义存放entry对象的Entry数组
    useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 
    init();
}
//从上方就可看出JDK7在实例化后就创建好了一个容量为16的Entry数组
//put方法添加数据
public V put(K key, V value) {
    if (key == null)//如果key是null,HashMap单独处理放入HashMap中
        return putForNullKey(value);
    int hash = hash(key);//调用hash()计算得到当前key的哈希值
    int i = indexFor(hash, table.1ength);//调用indexFor()并传入hash值和Entry数组table的长度计算key-value存放在数组中的哪个位置
    for(Entry<K,V> e = table[i]; e != nu1l; e = e.next) {//判断计算出的位置上有没有已存在的数据,如果能进入循环说明该位置上已经有数据了    e.next是指向下一条数据,准备与下一条数据进行比较
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        //if(比较已存在数据和新添加数据的hash值是否相同  且  比较已存在数据和新添加数据的key是否相同)
        //若其中一个条件为false,则不满足两条数据相同,跳出循环,将新数据添加到HashMap中
        //若两个条件都为true,说明两条数据确实相同。则执行下方操作替换掉已存在数据的value
            V oldValue = e.value; //将已存在数据的value赋值给oldValue
            e.value = value; //将新添加的数据的value赋值给已存在的数据的value实现覆盖替换
            e.recordAccess(this);
            return oldValue;//将被覆盖替换的值返回,不再执行下方添加数据的代码
        }
    }
    modCount++;
    addEntry(hash, key, value, i);//若经过判断后新数据能被添加,则调用addEntry()传入hash值、key-value对和要添加到的位置,将这个需要添加的数据添加到HashMap中
    return null;
}
//计算hash值的方法
final int hash(Object k) {
    int h=0;
    if (useAltHashing) {//默认false
        if (k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }
    h ^= k.hashCode();//调用hashCode()得到key的哈希值,然后进行异或计算
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default Load factor).
    h ^= (h>>>20) ^ (h>>>12);
    return h ^ (h>>>7) ^ (h>>>4);//返回经过一些计算后的hash值
}
//计算存放位置
static int indexFor(int h, int 1ength) {
    return h & (1ength-1);
}
//添加数据Entry对象到HashMap中的具体方法
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (nu1l != table[bucketIndex])) {//判断是否将数组table进行扩容
    //if(判断数组table中已存有的key-value对个数是否超过扩容临界值  且  确认需要添加新数据的位置是否已存有其他数据)
    //如果已存有key-value对个数大于等于扩容临界值,但新数据将要添加到的位置上为null,则不需要进行扩容
    //或者如果需要添加新数据的位置上不为null,但已存有key-value对个数不大于等于扩容临界值,也不进行扩容
    //即既当需要添加新数据的位置上不为null,又已存有key-value对个数大于等于扩容临界值时才进行扩容
        resize(2 * table.length);//扩容为原来容量的2倍,并将原有的数据进行重新计算hash值,然后放入新数组的新位置
        hash = (nu1l != key) ? hash(key) : 0;//将这个需要新添加的数据的hash值进行重新计算
        bucketIndex = indexFor(hash, table.length);//重新计算存放位置
    }
    //不需要进行扩容或扩容后添加新数据
    createEntry(hash, key, value, bucketIndex);
}
//添加新数据
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];//把此位置上原有的数据取出来
    table[bucketIndex] = new Entry<>(hash, key, value, e);//然后在此位置上new一个新的存放新数据的Entry对象,并将原来的数据传入,以新数据.next=原数据的方式让新数据指向原来的数据
    size++;//key-value对的个数加1
}

JDK8

//无参构造器
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; //负载因子(填充因子),默认0.75
}
//有参构造器
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
//从上方可看出实例化HashMap后没有创建好长度为16的Node数组,而是等到下方第一次调用put方法后才创建的
//新增数据调用的方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);//将计算好后的hash值、新数据的key、value等传入putVal()
}
//计算hash值的方法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//新增数据时内部调用的方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; 
    Node<K,V> p; 
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)//判断是否首次添加数据,若是则给当前HashMap对象创建新数组
        n = (tab = resize()).length;//调用resize()给当前HashMap对象创建新数组进行初始化
    if ((p = tab[i = (n - 1) & hash]) == null)//经过(n-1)&hash计算后得到新数据存放的位置,并判断此处是否为null
        tab[i] = newNode(hash, key, value, null);//若是为null,添加成功
    else {//不是首次添加且新数据需要的存放位置不为null
        Node<K,V> e; 
        K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
        //比较已存在的数据的hash值、key值地址和新数据的hash值、key值地址 || 若hash值一样,key值地址不一样,则比较key值的具体内容
            e = p;//若hash值和key值都一样,说明两条数据一样,则将已存在的数据取出赋给e,到下方进行key-value对的value替换操作
        else if (p instanceof TreeNode)//若已存在数据和新数据的hash值和key值不一样,判断已存在该位置上的数据是否是以红黑树结构存在
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//若是,将已存在的数据
        else {
            //已存在数据和新数据的hash值和key值不一样,且已存在该位置上的数据不是以红黑树结构存在
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {//寻找该位置上以链表形式存在的最后一个数据
                    p.next = newNode(hash, key, value, null);//让新数据添加到该链表已存在的最后一个数据后面
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);//如果添加该新数据后,该链表上的数据个数大于等于树化临界值-1,则将该条链表转换为红黑树进行存储
                    break;//跳出循环
                }
                //寻找该位置上以链表形式存在的最后一个数据时会与该链表上的每个数据进行比较hash值、key值和value值
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;//如果发现有一样的数据,跳出循环
                p = e;//若正在进行比较的数据既不是该链表的最后一个也不和新数据一样,则将正在比较的数据赋给p,执行下一次循环p.next,即继续遍历判断下一个数据(p是当前数据,e是下一个数据)
            }
        }
        if (e != null) { // existing mapping for key  
            V oldValue = e.value;//新value替换旧value
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)//添加完新数据后判断key-value对的个数是否大于扩容临界值
        resize();//若大于扩容临界值,执行扩容操作
    afterNodeInsertion(evict);
    return null;
}
//扩容的方法
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//第一次添加数据时,table=null
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//改变容量前的数组的容量,第一次调用则为0
    int oldThr = threshold;//改变容量前的数组的扩容临界值,第一次调用默认为0
    int newCap, newThr = 0;//声明新容量,新扩容临界值
    if (oldCap > 0) {//判断旧数组的容量是否大于0,即是不是第一次调用resize()
        if (oldCap >= MAXIMUM_CAPACITY) {//若旧数组的容量大于等于设置的最大容量
            threshold = Integer.MAX_VALUE;//设置扩容临界值为Integer类中的最大值
            return oldTab;//返回重新设置了扩容临界值的旧数组
        }
        //旧数组容量小于设置的最大容量,对旧数组进行扩容
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
        //if(扩容为原来2倍后的新容量小于最大容量  且  旧数组容量大于等于默认初始化容量16)
            newThr = oldThr << 1; //新数组的扩容临界值扩大为原来的2倍
    }
    else if (oldThr > 0) //initial capacity was placed in threshold
    //判断如果旧数组的容量=0但扩容临界值大于0的情况
    //(能到这儿说明是设置了初始化的扩容临界值,未设置容量)
        newCap = oldThr;//若是,则数组的新容量就为旧的扩容临界值
    else {               // zero initial threshold signifies using defaults
        //实例化数组时,容量为0,扩容临界值也为0,进行下方数组的初始化操作
        newCap = DEFAULT_INITIAL_CAPACITY;//容量设置为16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//扩容临界值设置为12
    }
    if (newThr == 0) {//判断新的扩容临界值是否还是等于0
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    //新的扩容临界值不为0,新容量也已设置,准备条件已就绪
    threshold = newThr;//将得到的新扩容临界值赋值给当前HashMap对象的扩容临界值
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//根据新的容量创建一个新数组
    table = newTab;//将新数组赋给当前HashMap对象的数组,自此就已创建好新数组,若是实例化第一次调用该方法,到此已结束,只需将新数组返回即可
    if (oldTab != null) { //不是第一次时调用该方法的话,扩容后会将原来的数据复制到新数组中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
//添加新数据后该链表的数据个数大于等于树化临界值-1则转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {//传入HashMap对象底层的数组table和新数据的hash值
    int n, index;
    Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();//判断table是否为null(一般不可能),或者table的长度是否小于最小树化容量,若比它小,则选择进行扩容操作,而不是树化
    else if ((e = tab[index = (n - 1) & hash]) != null) {//否则,执行下方代码将该链表进行树化
        //判断根据新数据的hash值计算出的table中对应的位置上是否为null,不为null则将该位置上的链表进行树化操作
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

10.3.3 LinkedHashMap类

底层实现原理
Map map = new HashMap();
Map map1 = new LinkedHashMap();
map.put(123,"AA");
map.put(456,"BB");
map.put(789,"CC");
map1.put(123,"AA");
map1.put(456,"BB");
map1.put(789,"CC");
System.out.println(map);//{789=CC, 456=BB, 123=AA}
System.out.println(map1);//{123=AA, 456=BB, 789=CC}

LinkedHashMap中没有put方法,而是继承自HashMap,在put方法中又调用HashMap的putVal方法,但是在这个putVal方法中有一个newNode方法,而LinkedHashMap重写了这个方法,从此处就能区别开HashMap与LinkedHashMap的存储结构差异

//HashMap中的Node类(仅包括定义的属性)
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}
//继承自HashMap的Entry类
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;//新增的属性,用来指明一个数据结点的上一个数据和下一个数据。记录增添数据的先后顺序
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }    
}
//覆盖重写HashMap中的newNode方法。所以LinkedHashMap创建的存储数据的结点会不一样,即多了before和after属性
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
//指明新数据结点的上一个和下一个数据
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

10.3.4 TreeMap类

//向TreeMap中添加key-value,要求key必须是由同一个类创建的对象
//因为要按照key进行排序:自然排序、定制排序
//具体细节可看10.2.3中的第3点TreeSet,因为TreeSet的实现底层就是TreeMap。两者相差不大,只是存放的数据类型不同
//自然排序,User类中重写的compareTo()方法是按名字从大到小,再按年龄从小到大排序
TreeMap treeMap = new TreeMap();
User user1 = new User("Tom",10);
User user2 = new User("Jack",50);
User user3 = new User("Jerry",25);
User user4 = new User("Eric",20);
User user5 = new User("Eric",23);
treeMap.put(user1,98);
treeMap.put(user2,94);
treeMap.put(user3,89);
treeMap.put(user4,87);
treeMap.put(user5,98);
Set users = treeMap.entrySet();
Iterator iterator = users.iterator();
while(iterator.hasNext()) {
    Object obj = iterator.next();
    Map.Entry entry = (Map.Entry) obj;
    System.out.print(entry.getKey() + "  -->  ");
    System.out.println(entry.getValue());
    //User{name='Tom', age=10}  -->  98
    //User{name='Jerry', age=25}  -->  89
    //User{name='Jack', age=50}  -->  94
    //User{name='Eric', age=20}  -->  87
    //User{name='Eric', age=23}  -->  98
}
//定制排序
Comparator comparator = new Comparator() {
    //按年龄从小到大排序
    @Override
    public int compare(Object o1, Object o2) {
        if(o1 instanceof User && o2 instanceof User){
            User user1 = (User) o1;
            User user2 = (User) o2;
            return Integer.compare(user1.getAge(),user2.getAge());
        }
        throw new RuntimeException("比较的类型不正确");
    }
};
TreeMap treeMap = new TreeMap(comparator);
User user1 = new User("Tom",10);
User user2 = new User("Jack",50);
User user3 = new User("Jerry",25);
User user4 = new User("Eric",20);
User user5 = new User("Eric",23);
treeMap.put(user1,98);
treeMap.put(user2,94);
treeMap.put(user3,89);
treeMap.put(user4,87);
treeMap.put(user5,98);
Set users = treeMap.entrySet();
Iterator iterator = users.iterator();
while(iterator.hasNext()) {
    Object obj = iterator.next();
    Map.Entry entry = (Map.Entry) obj;
    System.out.print(entry.getKey() + "  -->  ");
    System.out.println(entry.getValue());
    //User{name='Tom', age=10}  -->  98
    //User{name='Eric', age=20}  -->  87
    //User{name='Eric', age=23}  -->  98
    //User{name='Jerry', age=25}  -->  89
    //User{name='Jack', age=50}  -->  94
}

10.3.5 Properties类

对于HashTable来说,Hashtable是个古老的Map实现类,JDK1.0就提供了,也是Map接口的第四个实现类 。不同于HashMap,Hashtable是线程安全的

Hashtable实现原理和HashMap相同,功能相同底层都使用哈希表结构,查询速度快,很多情况下可以互用

  • 与HashMap不同,Hashtable不允许使用null作为key和value
  • 与HashMap一样,Hashtable 也不能保证其中Key-Value对的顺序
  • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致

Properties类是Hashtable的子类,该对象用于处理属性文件,由于属性文件里的key、value都是字符串类型,所以Properties 里的key和value都是字符串类型

存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

Properties pros = new Properties();
FileInputStream in = new FileInputStream("jdbc.properties");
pros.load(in);
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name = " + name + ", password = " + password);//name = Tom, password = abc123

10.4 Collections工具类

面试题:Collection 和Collections的区别?

Collection是一个存储单列数据集合的接口,List、Set都继承自它
Collections是一个操作List、Set和Map的工具类

Collections 是一个操作Set、 List和Map等集合的工具类,Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

10.4.1 Collections中的常用方法

1.排序:
  • reverse(List):反转List中元素的顺序
  • shuffle(List):对List集合元素进行随机排序
  • sort(List):根据元素的自然顺序对指定List集合元素按升序排序
  • sort(List, Comparator):根据指定的Comparator产生的顺序对List 集合元素进行排序
  • swap(List list,int i,int j):将指定list集合中的i处元素和j处元素进行交换
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(-97);
list.add(0);
System.out.println(list);//[123, 43, 765, -97, 0]
//reverse(List):反转List中元素的顺序
Collections.reverse(list);
System.out.println(list);//[0, -97, 765, 43, 123]
//shuffle(List):对List集合元素进行随机排序
Collections.shuffle(list);
System.out.println(list);//第一次[0, 123, 43, -97, 765]  第二次[123, 765, 43, -97, 0]
//sort(List):根据元素的自然顺序对指定List集合元素按升序排序
Collections.sort(list);
System.out.println(list);//[-97, 0, 43, 123, 765]
//swap(List list,int i,int j):将指定list集合中的i处元素和j处元素进行交换
Collections.swap(list,1,3);
System.out.println(list);//[-97, 123, 43, 0, 765]
2.查找、替换:
  • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection,Comparator): 根据Comparator指定的顺序,返回给定集合中的最小元素
  • Object min(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  • Object min(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最小元素
  • int frequency(Collection, Object):返回指定集合中指定元素的出现次数
  • void copy(List dest,List src):将src中的内容复制到dest中
  • boolean replaceAll(List list, Object oldVal, Object newVal):使用新值newVal替换List对象的所有旧值oldVal
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(765);
list.add(-97);
list.add(765);
list.add(0);
System.out.println(list);//[123, 43, 765, 765, -97, 765, 0]
//Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
System.out.println(Collections.max(list));//765
//Object min(Collection):根据元素的自然顺序,返回给定集合中的最大元素
System.out.println(Collections.min(list));//-97
//int frequency(Collection, Object):返回指定集合中指定元素的出现次数
System.out.println(Collections.frequency(list, 765));//3
//void copy(List dest,List src):将src中的内容复制到dest中
//错误写法:
//List list1 = new ArrayList();
//Collections.copy(list1,list);
List list1 = Arrays.asList(new Object[list.size()]);
Collections.copy(list1,list);
System.out.println(list1);//[123, 43, 765, 765, -97, 765, 0]
//boolean replaceAll(List list, Object oldVal, Object newVal):使用新值newVal替换List对象的所有旧值oldVal
Collections.replaceAll(list,765,567);
System.out.println(list);//[123, 43, 567, 567, -97, 567, 0]
3.转换为线程安全的集合

Collections类中提供了多个synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

在Collections类中有几个定义的SynchronizedXxx的静态内部类,当调用这些SynchronizedXxx()方法时,会在这个方法内部创建一个对应的线程安全的内部类对象,然后返回这个对象。当我们使用返回的对象调用List、Set和Map等的方法时,都会调用到Collections定义的几个静态内部类中重写的相对应的方法,以此来保证线程的安全性
//以List为例
List list = new ArrayList();
List list1 = Collections.synchronizedList(list);//此时返回的list即为线程安全的
//Collections中的synchronizedList()方法
public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}
//定义的SynchronizedList静态内部类类
static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }
    SynchronizedList(List<E> list, Object mutex) {
        super(list, mutex);
        this.list = list;
    }

    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {return list.equals(o);}
    }
    public int hashCode() {
        synchronized (mutex) {return list.hashCode();}
    }

    public E get(int index) {
        synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }

    public int indexOf(Object o) {
        synchronized (mutex) {return list.indexOf(o);}
    }
    public int lastIndexOf(Object o) {
        synchronized (mutex) {return list.lastIndexOf(o);}
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        synchronized (mutex) {return list.addAll(index, c);}
    }

    public ListIterator<E> listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }

    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }

    public List<E> subList(int fromIndex, int toIndex) {
        synchronized (mutex) {
            return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                          mutex);
        }
    }

    @Override
    public void replaceAll(UnaryOperator<E> operator) {
        synchronized (mutex) {list.replaceAll(operator);}
    }
    @Override
    public void sort(Comparator<? super E> c) {
        synchronized (mutex) {list.sort(c);}
    }

    /**
         * SynchronizedRandomAccessList instances are serialized as
         * SynchronizedList instances to allow them to be deserialized
         * in pre-1.4 JREs (which do not have SynchronizedRandomAccessList).
         * This method inverts the transformation.  As a beneficial
         * side-effect, it also grafts the RandomAccess marker onto
         * SynchronizedList instances that were serialized in pre-1.4 JREs.
         *
         * Note: Unfortunately, SynchronizedRandomAccessList instances
         * serialized in 1.4.1 and deserialized in 1.4 will become
         * SynchronizedList instances, as this method was missing in 1.4.
         */
    private Object readResolve() {
        return (list instanceof RandomAccess
                ? new SynchronizedRandomAccessList<>(list)
                : this);
    }
}

10.4.2 集合练习题

1.从键盘随机输入10个整数保存到List中,并按倒序、从大到小的顺序显示出来
2.把学生名与考试分数录入到集合中,并按分数显示前三名成绩学员的名字。TreeSet(Student(name,score,id));
3.姓氏统计:一个文本文件中存储着北京所有高校在校生的姓名,格式如下
每行一个名字,姓与名以空格分隔:
张  三
李  四
王  小五
现在想统计所有的姓氏在文件中出现的次数,请描述一下你的解决方案
4.对一个Java源文件中的关键字进行计数
提示:Java源文件中的每一个单词,需要确定该单词是否是一个关键字。为了高效处理这个问题,将所有的关键字保存在一个HashSet中。用contains()来测试
File file = new File("Test.java");
Scanner scanner = new Scanner(file);
while(scanner.hasNext()){
    String word = scanner.next();
    System.out.printIn(word);
}

十一、Java泛型

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。collection<E>List<E>ArrayList<E>这个**就是类型参数,即泛型**

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)

从JDK1.5以后,Java引入了“参数化类型( Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List,这表明该List只能保存字符串类型的对象

JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持从而可以在声明集合变量、创建集合对象时传入类型实参

11.1 泛型的使用

泛型的使用
1.jdk 5.0新增的特性
2.在集合中使用泛型:
总结:
① 集合接口或集合类在jdk5.0时都修改为带泛型的结构
② 在实例化集合类时,可以指明具体的泛型类型
③ 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。比如: add(E e) ---> 实例化以后: add(Integer e)
④ 注意点:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换
⑤ 如果实例化时,没有指明泛型的类型。默认类型为java.lang.object类型
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(78);
list.add(87);
list.add(99);
list.add(65);
//编译时,就会进行类型检查,保证数据的安全
//list.add("Tom");
//遍历方式一:
for(Integer score : list){
    //避免了强转操作
    int stuScore = score;
    System.out.println(stuScore);
}
//遍历方式二:
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
    int stuScore1 = iterator.next();
    System.out.println(stuScore1);
}
 //以HashMap为例
 Map<String,Integer> map = new HashMap<String,Integer>();
 map.put("Tom",87);
 map.put("Jerry",87);
 map.put("Jack",87);
 //编译报错
 //map.put(123,"Mary");
 //泛型的嵌套
 Set<Map.Entry<String,Integer>> entry = map.entrySet();
 Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
 while (iterator.hasNext()){
     Map.Entry<String, Integer> e = iterator.next();
     String key = e.getKey();
     Integer value = e.getValue();
     System.out.println(key + "  -->  " + value);
 }

11.2 自定义泛型

11.2.1 自定义泛型类和接口

自身定义为泛型类(接口)

//如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为Object类型
//要求:如果定义了类是带泛型的,建议在实例化时要指明类的泛型
public class Order<T> {
    String orderName;
    int orderId;
    //类的内部结构可以使用类的泛型
    T orderT;
    public Order() {
    }
    public Order(String orderName, int orderId, T orderT) {
        this.orderName = orderName;
        this.orderId = orderId;
        this.orderT = orderT;
    }
    public T getOrderT() {
        return orderT;
    }
    public void setOrderT(T orderT) {
        this.orderT = orderT;
    }
}
Order order = new Order();
order.setOrderT(123);
order.setOrderT("ABC");
//实例化时指明类的泛型
Order<String> order1 = new Order<String>("orderAA",1001,"order:AA");
order1.setOrderT("AA:hello");//此时只能设置String类型

其他类继承泛型类(实现接口)

public class SubOrder extends Order<Integer>{//非泛型类
}
public class SubOrder1<T> extends Order<T>{//泛型类
}
SubOrder subOrder = new SubOrder();
//由于子类在继承带泛型的父类时,指明了泛型类型。则实例化子类对象时,不再需要指明泛型
subOrder.setOrderT(1002);//此时传入的类型只能是继承Order<Integer>时指明的Integer类型,若没有像如此指明类型即是继承自Order<T>,则传入的数据类型是Object类型

SubOrder1<String> subOrder = new SubOrder1<String>();//是泛型类
subOrder1.setOrderT("order2...");

自定义类和接口的注意点:

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>

  2. 泛型类的构造器如下:**public GenericClass(){}。而下面是错误的: public GenericClass(){},而实例化时需要new Generic<>();**

  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致

  4. 泛型不同的引用不能相互赋值。尽管在编译时ArrayList<String>ArrayList<Integer>是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中

    ArrayList<String> list1 = nu1l;
    ArrayList<Integer> list2 = nul1;
    list1 = list2;//泛型不同的引用不能相互赋值
    Person p1 = null;
    Person p2=null;
    p1 = p2;//而这种普通引用可以
    
  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。 经验:泛型要使用一路都用。要不用,一路都不要用

  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象

  7. jdk1.7, 泛型的简化操作: ArrayList<Fruit> flist = new ArrayList<>();

  8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换

  9. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型

    public static void show(T orderT){//编译报错
        Systrem.out.println(orderT);
    }
    //因为泛型是针对实例化对象使用来指定类型的,而静态方法是早于实例化对象创建,在没有实例化对象时也要进行创建和调用
    
  10. 异常类不能是泛型的

  11. 不能使用new E[]。但是可以: E[] elements = (E[])new Object[capacity];
    参考: ArrayList源码中声明: Object[] elementData,而非泛型参数类型数组

  12. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现
      • 没有类型擦除
      • 具体类型
    • 子类保留父类的泛型:泛型子类
      • 全部保留
      • 部分保留
    class Father<T1, T2> {
    }
    //子类不保留父类的泛型
    // 1)没有类型 擦除
    class Son1 extends Father {//等价于class Son extends Father<Object , 0bject>{
    }
    // 2)具体类型
    class Son2 extends Father<Integer, String> {
    }
    //子类保留父类的泛型
    // 1)全部保留
    class Son3<T1, T2> extends Father<T1, T2> {
    }
    // 2)部分保留
    class Son4<T2> extends Father<Integer, T2> {
    }
    
    class Father<T1, T2> {
    }
    //子类不保留父类的泛型
    // 1)没有类型擦除
    class Son<A, B> extends Father{//等价于class Son extends Father<Object , 0bject>{
    }
    // 2)具体类型
    class Son2<A, B> extends Father<Integer, String> {
    }
    //子类保留父类的泛型
    // 1)全部保留
    class Son3<T1, T2, A, B> extends Father<T1, T2> {
    }
    // 2)部分保留
    class Son4<T2, A, B> extends Father<Integer, T2> {
    }
    
  13. 结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型

11.2.2 自定义泛型方法

public T getOrderT() {
    return orderT;
}
public void setOrderT(T orderT) {
    this.orderT = orderT;
}
boolean add(E e);
//以上三种不是泛型方法,而是普通方法

<T> T[] toArray(T[] a);//这是一个泛型方法
//泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系,换句话说,泛型方法所属的类是不是泛型类都没有关系,所以在非泛型类中也可以定义使用 
//泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的,并非在实例化类时确定
//定义时需要在前方加上类似<E>的标识来告诉编译器这是一个泛型方法,其泛型和类没有任何关系
public <E> List<E> copyFromArrayTolist(E[] arr){ 
    ArrayList<E> list = new ArrayList<>();
    for(E e : arr){
    list.add(e);
    return list;
}
Order<String> order = new 0rder<>();
Integer[] arr = new Integer[]{1,2,3,4};
List<Integer> list = order.copyFromArrayToList(arr);
System.out.println(list);//[1,2,3,4]

11.3 泛型在继承方面的体现

虽然类A是类B的父类,但是G 和G 二者不具备子父类关系,二者是并列关系

补充:类A是类B的父类,A是B的父类

Object obj = null;
String str = null;
obj = str;

Object[] arr1 = null;
String[] arr2 = null;

arr1 = arr2;
//编译不通过
//Date date = new Date();
//str = date;

List<Object> list1 = null;
List<String> list2 = null;
//此时的list1和list2的类型不具有父子关系,编译不通过
//list1 = list2;
/*
    反证法:
    假设list1 = list2;
    list1.add(123);导致混入非String的数据。出错。
*/

11.4 通配符的使用

使用类型通配符:?,比如:List<?>Map<?,?>

List<?>List<String>List<Object>等各种泛型List的父类。比如,类A是类B的父类,但G<A>G<B>是没有关系的,二者共同的父类是G<?>

11.4.1 使用通配符后对数据的写入和读取

1.写入数据

不能向使用通配符后的集合写入数据。因为我们不知道要添加的元素类型,就向其中添加对象

将任意元素加入到其中都不是类型安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); //编译时错误
//add方法有类型参数E作为集合的元素类型,我们传给add的任何参数都必须是这个未知类型的子类。因为我们不知道c的元素类型,自然也就不知道其子类,所以我们无法添加任何东西进去

//唯一的例外的是null,它是所有类型的成员
2.读取数据

我们可以读取到使用通配符后的集合对象中的数据。比如,读取List<?>的对象list中的元素时,永远是安全的,因为我们调用get()方法并得到其返回值后,不管其数据的真实类型是什么,它都总是一个Object

List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<?> list = null;
list = list1;//能赋值成功
list = list2;//能赋值成功

List<String> list3 = new ArrayList<>();
list3.add("AA");
list3.add("BB");
list3.add("CC");
list = list3;
//添加:对于List<?>就不能向其内部添加数据,除了添加null
//list.add("DD");
//获取:允许读取数据,读取的数据类型为Object
Object o =list.get(0);
System.out.println(o);

11.4.2 有限制的通配符

  • <?>允许所有泛型的引用调用
  • 通配符指定上限,上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
  • 通配符指定下限,下限super:使用时指定的类型不能小于操作的类,即>=
  • 举例:
    • (无穷小,Number] :**只允许泛型为Number及Number子类的引用调用**
    • [Number ,无穷大) :**只允许泛型为Number及Number父类的引用调用**
    • :**只允许泛型为实现Comparable接口的实现类的引用调用**
//? extends A:
//        G<? extends A>可以作为G<A>和G<B>的父类,其中B是A的子类
//? super A:
//        G<? super A>可以作为G<A>和G<B>的父类,其中B是A的父类
List<? extends Person> list1 = null;
List<? super Person> list2 = null;

List<Student> list3 = null;
List<Person> list4 = null;
List<Object> list5 = null;

list1 = list3;
list1 = list4;
//list1 = list5;//编译失败,因为Object是Person的父类,而? extends Person指明的集合只能由Person及其子类可以调用

//list2 = list3;//编译失败,因为Student是Person的子类,而? super Person指明的集合只能由Person及其父类可以调用
list2 = list4;
list2 = list5;

//读取数据:
list1 = list3;
Person p = list1.get(0);//Object o = list1.get(0)也可以获取,但声明的类型最小只能是Person,不能是Student
//编译不通过
//Student s = list1.get(0);

list2 = list4;
//编译不通过
//Person p1 = list2.get(0);
Object o = list2.get(0);//可以获取,但声明的类型只能是Object

//写入数据
//编译不通过
//list1.add(new Person());//添加的Person类型有是list1中数据的类型的父类的可能,即?指的数据类型可能比Person更小
//list1.add(new Student());//添加的Student类型也有是list1中数据的类型的父类的可能,即?指的数据类型可能比Student更小

//编译通过
list2.add(new Person());//?指的是Person及其父类,那么list2中存的数据类型最小都是Person类型和其子类,所以Person类型可以添加
list2.add(new Student());//Student是Person的子类,所以也可以添加
//list2.add(new Object());//不能添加因为有可能?指的是Person和Object之间的类型,那么此时Object就是其中数据类型的父类,则添加不成功

11.5 练习

定义个泛型类DAO<T>,在其中定义一个Map成员变量,Map的键为String类型,值为T类型。
分别创建以下方法: 
public void save(String id,T entity):保存T类型的对象到Map成员变量中
public T get(Stringid):从map中获取id 对应的对象
public void update(String id,T entity):替换map中key为id的内容,改为entity对象
public List<T> list():返回map中存放的所有T对象
public void delete(String id):删除指定id 对象

定义一个User类:
该类包含: private成员变量(int类型) id, age; (String类型) name

定义一个测试类:
创建DAO类的对象,分别调用其save、get、update、list、 delete方法来操作User对象
使用Junit 单元测试类进行测试
public class DAO<T> {
    //由于存入map的key是单独的String类型,则key-value的存储位置是String类中的HashCode()和equals()决定
    // 所以就没有必要在User中重写HashCode(),但是equals()有必要重写,因为可能会涉及到判断key对应的value值的一些情况
    public Map<String,T> map = new HashMap<>();

    public void save(String id,T entity){
        map.put(id,entity);
    }
    public T get(String id){
        return map.get(id);
    }
    public void update(String id,T entity){
        if (map.containsKey(id)){
            map.put(id,entity);
        }
        //map.replace(id,entity);//方法二
    }
    public List<T> list(){
        //错误的
        //Collection<T> values = map.values();
        //return (List<T>)values;
        //正确的
        ArrayList<T> list = new ArrayList<>();
        Collection<T> values = map.values();
        for (T value : values) {
            list.add(value);
        }
        return list;
    }
    public void delete(String id){
        map.remove(id);
    }
}
public class User {
    private int id;
    private int age;
    private String name;

    public User() {
    }
    public User(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        if (id != user.id) return false;
        if (age != user.age) return false;
        return name.equals(user.name);
    }
}
public class DAOTest {
    @Test
    public void Test(){
        DAO<User> userDAO = new DAO<>();
        User user1 = new User(1,10,"Tom");
        User user2 = new User(2,34,"Jerry");
        User user3 = new User(3,23,"Jack");
        User user4 = new User(4,14,"Eric");
        User user5 = new User(5,25,"Mary");
        userDAO.save("1001",user1);//save方法测试
        userDAO.save("1002",user2);
        userDAO.save("1003",user3);
        userDAO.save("1004",user4);
        userDAO.save("1005",user5);
        List<User> list = userDAO.list();//list()方法测试
        list.forEach(System.out::println);
        System.out.println("======================");
        userDAO.update("1003",new User(3,25,"Jack"));//update()方法测试
        List<User> list1 = userDAO.list();
        list1.forEach(System.out::println);
        System.out.println("======================");
        User user = userDAO.get("1002");//get()方法测试
        System.out.println(user);
        System.out.println("======================");
        userDAO.delete("1004");//delete()方法测试
        List<User> list2 = userDAO.list();
        list2.forEach(System.out::println);
        //输出结果:
        /*
            User{id=5, age=25, name='Mary'}
            User{id=4, age=14, name='Eric'}
            User{id=3, age=23, name='Jack'}
            User{id=2, age=34, name='Jerry'}
            User{id=1, age=10, name='Tom'}
            ======================
            User{id=5, age=25, name='Mary'}
            User{id=4, age=14, name='Eric'}
            User{id=3, age=25, name='Jack'}
            User{id=2, age=34, name='Jerry'}
            User{id=1, age=10, name='Tom'}
            ======================
            User{id=2, age=34, name='Jerry'}
            ======================
            User{id=5, age=25, name='Mary'}
            User{id=3, age=25, name='Jack'}
            User{id=2, age=34, name='Jerry'}
            User{id=1, age=10, name='Tom'}
         */
    }
}

十二、Java IO流

12.1 File类的使用

java.io.File类:文件和文件目录路径的抽象表示形式,与平台无关,**File类的一个对象,代表一个文件或一个文件目录(**俗称:文件夹)

File能新建、删除、重命名文件和目录,但File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流(Input/Output),到时File对象会经常作为参数传递给流的构造器

想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录

当一个File对象是否有在硬件中对应的文件或文件目录存在或不存在时:

12.1.1 常用构造器

public File(String pathname):以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储

  • 绝对路径:是一个固定的路径,从盘符开始
  • 相对路径:是相对于某个位置开始

public File(String parent,String child):以parent为父路径,child为子路径创建File对象

public File(File parent,String child):根据一个父File对象和子文件路径创建File对象

//实例化:此时实例化出来的File对象都只是内存层面的,还没有真正的创建出一个文件或目录到硬盘层面
//相对路径:相较于某个路径下,指明的路径
//绝对路径:包含盘符在内的文件或文件目录的路径

//创建实例化对象方式一:public File(String pathname)
File file = new File("hello.txt");//相对路径,相对于当前项目或模块下
File file1 = new File("E:\\桌面文件\\IOTest\\hello.txt");//绝对路径
System.out.println(file);//hello.txt
System.out.println(file1);//E:\桌面文件\IOTest\hello.txt

//创建实例化对象方式二:public File(String parent,String child)
File file2 = new File("E:\\桌面文件\\IOTest","IOHello");//parent路径指的是我们要创建文件或目录到哪个文件目录下,而child是我们要创建的文件或目录的名字
System.out.println(file2);//E:\桌面文件\IOTest\IOHello

//创建实例化对象方式三:public File(File parent,String child)
File file3 = new File(file2,"hello.txt");//这里的parent是个File对象,说明是我们预先创建好的一个File对象,然后在这个对象(目录)下再创建一个新的文件或目录
System.out.println(file3);//E:\桌面文件\IOTest\IOHello\hello.txt

12.1.2 路径分隔符

路径中的每级目录之间用一个路径分隔符隔开。

路径分隔符和系统有关:

  • windows和DOS系统默认使用“\” 来表示

  • UNIX和URL使用“/”来表示

Java程序支持跨平台运行,因此路径分隔符要慎用。为了解决这个隐患,File类提供了一个常量:public static final String separator。根据操作系统,动态的提供分隔符

举例:

File file1 = new File("d: \\atguigu\\info.txt");
File file2 = new File("d:" + File.separator + "atguigu" + File.separator + "info.txt");
File file3 = new File("d:/atguigu");

12.1.3 File类的常用方法

1.File类的获取功能
  • public String getAbsolutePath():获取绝对路径
  • public String getPath():获取路径
  • public String getName():获取名称
  • public String getParent():获取上层文件目录路径。若无,返回null
  • public long length():**获取文件长度(即:字节数)**。不能获取目录的长度
  • public long lastModified():获取最后次的修改时间,毫秒值
  • public String[] list():获取指定目录下的所有文件或者文件目录的名称数组(适用于文件目录)
  • public File[] listFiles():获取指定目录下的所有文件或者文件目录的File数组(适用于文件目录)
File file1 = new File("hello.txt");//相对路径
File file2 = new File("E:\\桌面文件\\IOTest\\hello.txt");//绝对路径
//在没有创建实体文件的情况下,length和lastModified都为0
//public String getAbsolutePath():获取绝对路径
System.out.println(file1.getAbsoluteFile());//E:\JavaProject\IDEAProject\自主练习\Demo\hello.txt
//public String getAbsolutePath():获取绝对路径
System.out.println(file1.getPath());//hello.txt
//public String getName():获取名称
System.out.println(file1.getName());//hello.txt
//public String getParent():获取上层文件目录路径。若无,返回null
System.out.println(file1.getParent());//null
//public long length():获取文件长度(即:字节数)。不能获取目录的长度
System.out.println(file1.length());//0
//public long lastModified():获取最后次的修改时间,毫秒值
System.out.println(file1.lastModified());//0
System.out.println();
System.out.println(file2.getAbsoluteFile());//E:\桌面文件\IOTest\hello.txt
System.out.println(file2.getPath());//E:\桌面文件\IOTest\hello.txt
System.out.println(file2.getName());//hello.txt
System.out.println(file2.getParent());//E:\桌面文件\IOTest
System.out.println(file2.length());//0
System.out.println(file2.lastModified());//0
File file = new File("E:\\JavaProject\\IDEAProject\\Exercises\\Demo\\src");
//public String[] list():获取指定目录下的所有文件或者文件目录的名称数组
String[] list = file.list();
for (String s : list) {
    System.out.println(s);//Annotation array Collection CommonClass enumeration Generic IO oop Test Thread
}
//public File[] listFiles():获取指定目录下的所有文件或者文件目录的File数组
File[] files = file.listFiles();
for (File file1 : files) {
    System.out.println(file1);
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\Annotation
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\array
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\Collection
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\CommonClass
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\enumeration
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\Generic
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\IO
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\oop
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\Test
    //E:\JavaProject\IDEAProject\Exercises\Demo\src\Thread
}
2.File类的重命名功能

public boolean renameTo(File dest):把文件重命名为指定的文件路径

//比如: file1.renameTo(file2)为例:
//要想保证返回true, 需要file1在硬盘中是存在的,而file2不能在指定路径的硬盘中已经存在
//当file1.renameTo(file2)成功后,再file2.renameTo(file1)也能将文件重新拿回来,说明文件需要被剪切到到的位置也可以用相对路径
File file1 = new File("hello.txt");
File file2 = new File("E:\\桌面文件\\IOTest\\hi.txt");
boolean b = file1.renameTo(file2);
System.out.println(b);
3.File类的判断功能
  • public boolean isDirectory():判断是否是文件目录
  • public boolean isFile():判断是否是文件
  • public boolean exists():判断是否存在
  • public boolean canRead():判断是否可读
  • public boolean canWrite():判断是否可写
  • public boolean isHidden():判断是否隐藏

File file = new File("hello.txt");
//public boolean isFile():判断是否是文件目录
System.out.println(file.isDirectory());//false
//public boolean isFile():判断是否是文件
System.out.println(file.isFile());//true
//public boolean exists():判断是否存在
System.out.println(file.exists());//true
//public boolean canRead():判断是否可写
System.out.println(file.canRead());//true
//public boolean canWrite():判断是否可写
System.out.println(file.canWrite());//true
//public boolean isHidden():判断是否隐藏
System.out.println(file.isHidden());//false
File file1 = new File("E:\\桌面文件\\IOTest");
System.out.println(file1.isDirectory());//true
System.out.println(file1.isFile());//false
System.out.println(file1.exists());//true
System.out.println(file1.canRead());//true
System.out.println(file1.canWrite());//true
System.out.println(file1.isHidden());//false//public boolean isDirectory():判断是否是文件目录
4.File类的创建功能
  • public boolean createNewFile():创建文件。若文件存在, 则不创建,返回false
  • public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建
  • public boolean mkdirs():创建文件目录。如果上层文件目录不存在, 一并创建
  • 注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目路径下
5.File类的删除功能
  • public boolean delete():删除文件或者文件夹
  • 删除注意事项:Java中的删除不走回收站
  • 删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录
//文件创建
File file = new File("hello.txt");
if(!file.exists()){//文件不存在
    //public boolean createNewFile():创建文件
    file.createNewFile();
    System.out.println("创建成功");
}else{//文件存在
    //public boolean delete():删除文件或者文件夹
    file.delete();
    System.out.println("删除成功");
}
//文件目录的创建
//当要创建的文件目录的上层目录存在时,mkdir和mkdirs的作用效果都一样
//当要创建的文件目录的上层目录不存在时,只有mkdirs能创建成功
File file1 = new File("E:\\桌面文件\\IOTest\\IOTest01");
boolean mkdir = file1.mkdir();
if (mkdir){
    System.out.println("创建成功1");
}
File file2 = new File("E:\\桌面文件\\IOTest\\IOTest02");
boolean mkdir1 = file2.mkdirs();
if(mkdir1){
    System.out.println("创建成功2");
}
6.File类练习
1.利用File构造器,new一个文件目录file
1)在其中创建多个文件和目录
2)编写方法,实现删除file中指定文件的操作

2.判断指定目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称

3.遍历指定目录所有文件名称,包括子文件目录中的文件。
拓展1:并计算指定目录占用空间的大小
拓展2:删除指定文件目录及其下的所有文件

12.2 IO流原理及流的分类

I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等

Java程序中,对于数据的输入/输出操作以“流(stream)”的方式进行

java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据

12.2.1 IO流的原理

12.2.2 流的分类

12.2.3 IO流的体系结构

12.3 节点流(或文件流)

12.3.1 FileReader类

从硬盘读入数据到内存

1.使用FileReader类read()实现数据的读入
//说明点:
//1. read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1
//2.异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理
//3.读入的文件一定要存在,否则就会报FileNotFoundException
@Test
public void TestFileReader() {
    FileReader fileReader = null;
    try {
        //1.实例化File类的对象,指明要操作的文件
        File file = new File("hello.txt");
        //2.提供具体的流
        fileReader = new FileReader(file);
        //3.数据读入
        //read():返回读入的一个字符,如果达到文件内容末尾,返回-1
        //方式一:
        //int data = fileReader.read();
        //while (data != -1){
        //    System.out.print((char) data);
        //  data = fileReader.read();
        //}
        //方式二:
        int data;
        while ((data = fileReader.read()) != -1){
            System.out.print((char) data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.流的关闭操作
        //(1)一般情况下是:先打开的后关闭,后打开的先关闭
        //(2)另一种情况:看依赖关系,如果流a依赖流b,应该先关闭流a,再关闭流b。例如,处理流a依赖节点流b,应该先关闭处理流a,再关闭节点流b
        //(3)可以只关闭处理流,不用关闭节点流。处理流关闭的时候,会调用其处理的节点流的关闭方法
        try {//此处的if和try-catch嵌套,谁在外面都行
            if(fileReader != null)
                fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2.使用FileReader类read(char[] cbuf)实现数据的读入
//对read()操作升级:使用read重载方法,一次性读取几个字符存入数组
@Test
public void TestFileReader1(){
    FileReader fileReader = null;
    try {
        //1.File类的实例化
        File file = new File("hello.txt");
        //2.FileReader流的实例化
        fileReader = new FileReader(file);
        //3.读入的操作
        //read(char cbuf):返回每次读入cbuf数组中的字符的个数,如果达到文件内容末尾,则返回-1
        char[] cbuffer = new char[5];
        int len;
        while ((len = fileReader.read(cbuffer)) != -1){
            //方法一:
            for (int i = 0; i < len; i++) {//不能写好i<cbuffer.length,因为每次读取数组中的字符都会覆盖上次读取,当最后几个字符的长度不足于填充满数组,那么最后一次遍历就会把没有被覆盖掉的几个字符也输出出来,所以我们应该读取几个字符就遍历输出几个
                System.out.print(cbuffer[i]);
            }
            //方法二:
            //String str = new String(cbuffer);
            //System.out.println(str);//也是错误的,跟上方for循环i<cbuffer.length一样的逻辑
            String str = new String(cbuffer,0,len);
            System.out.println(str);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.资源的关闭
        try {
        if (fileReader != null)
            fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.3.2 FileWriter类

从内存写出数据到硬盘

//说明:
//1.输出操作,对应的File可以不存在的。不会报异常
//2.File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件
//File对应的硬盘中的文件如果存在:
//        如果流使用的构造器是:Filewriter(file,false)/Filewriter(file):对原有文件进行覆盖
//        如果流使用的构造器是:Filewriter(file,true):不会对原有文件覆盖,而是在原有文件基础上追加内容
@Test
public void TestFileWriter(){
    FileWriter fileWriter = null;
    try {
        //1.实例化File类对象
        File file = new File("hello1.txt");
        //2.实例化FileWriter对象
        fileWriter = new FileWriter(file);
        //3.写出操作
        fileWriter.write("I have a dream!\n");
        fileWriter.write("you should have a dream,too!");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流
        try {
            if (fileWriter != null)
                fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.3.3 FileReader和FileWriter实现文本内容的复制

@Test
public void TestFileReaderFileWriter(){
    FileReader fileReader = null;
    FileWriter fileWriter = null;
    try {
        //1.创建File类的对象,指明读入和写出的文件
        File in = new File("hello1.txt");
        File out = new File("hello2.txt");
        //2.创建输入流和输出流的对象
        fileReader = new FileReader(in);
        fileWriter = new FileWriter(out);
        //3.数据的读入和写出操作
        char[] input = new char[5];
        int len;//记录每次读取到的字符个数
        while ((len = fileReader.read(input)) != -1){
            String output = new String(input,0,len);
            fileWriter.write(output);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流资源
        //方法一:
        try {
            if (fileReader != null)
                fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileWriter != null) 
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //方法二:
        try {
            if (fileReader != null)
                fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (fileWriter != null)
                fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.3.4 字符流不能处理图片的测试

@Test
public void TestCopyImage(){
    //测试失败,文件能复制,但不能打开。不能用字符流来处理图片等的字节数据
    FileReader fileReader = null;
    FileWriter fileWriter = null;
    try {
        //1.创建File类的对象,指明读入和写出的图片
        File in = new File("TestImage.jpg");
        File out = new File("TestImage1.jpg");
        //2.创建输入流和输出流的对象
        fileReader = new FileReader(in);
        fileWriter = new FileWriter(out);
        //3.数据的读入和写出操作
        char[] input = new char[5];
        int len;//记录每次读取到的字符个数
        while ((len = fileReader.read(input)) != -1){
            String output = new String(input,0,len);
            fileWriter.write(output);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileReader != null)
                fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (fileWriter != null)
                fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.3.5 FileInputStream不能读取文本的测试

//结论:可以使用字节流FileInputstream处理文本文件,但可能会出现中文乱码
//1.对于文本文件( .txt,.java,.c,.cpp),使用字符流处理(建议)
//2.对于非文本文件(.jpg,.mp3 , .mp4,.avi,.doc,.ppt,. . .),使用字节流处理(必须)
@Test
public void TestFileInputStreamText(){
    FileInputStream fileInputStream = null;
    try {
        //1.创建File对象
        File file = new File("hello.txt");
        //2.创建FileInputStream对象
        fileInputStream = new FileInputStream(file);
        //3.读取数据
        byte[] data = new byte[5];
        int len;//记录读取的字节个数
        while ((len = fileInputStream.read(data)) != -1){
            String str = new String(data,0,len);
            System.out.print(str);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流
        try {
            if (fileInputStream != null)
                fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.3.6 FileInputStream和FileOutputStream读写非文本文件

//能复制成功
@Test
public void TestFileInputOutputStream(){
    //实现图片的复制
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        //1.创建File类的对象,指明读入和写出的图片
        File in = new File("TestImage.jpg");
        File out = new File("TestImage1.jpg");
        //2.创建输入流和输出流的对象
        fileInputStream = new FileInputStream(in);
        fileOutputStream = new FileOutputStream(out);
        //3.数据的读入和写出操作
        byte[] input = new byte[5];
        int len;//记录每次读取到的字符个数
        while ((len = fileInputStream.read(input)) != -1){
            fileOutputStream.write(input,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileInputStream != null)
                fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (fileOutputStream != null)
                fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.3.7 FileInputStream和FileOutputStream复制文件的方法测试

//此时,复制成功的文本,点开文件查看不会乱码
//而在12.3.5中使用FileInputStream读入文本内容出现乱码是因为我们在读取数据后输出到控制台进行查看,这样会导致中文字符的字节被截断,而现在我们是在文本数据完整地读取和写出过后在文件中查看,此时中文字符的字节就不会被截断,所以就不会出现乱码
public void copy(String srcPath,String destPath){
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        //1.创建File类的对象,指明读入和写出的图片
        File in = new File(srcPath);
        File out = new File(destPath);
        //2.创建输入流和输出流的对象
        fileInputStream = new FileInputStream(in);
        fileOutputStream = new FileOutputStream(out);
        //3.数据的读入和写出操作
        byte[] input = new byte[1024];
        int len;//记录每次读取到的字符个数
        while ((len = fileInputStream.read(input)) != -1){
            fileOutputStream.write(input,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileInputStream != null)
                fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (fileOutputStream != null)
                fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Test
public void TestCopy(){
    String srcPath = "E:\\桌面文件\\IOTest\\[乒乓球].ts";
    String destPath = "E:\\桌面文件\\IOTest\\[乒乓球复制版].ts";
    long start = System.currentTimeMillis();
    copy(srcPath,destPath);
    long end = System.currentTimeMillis();
    System.out.println("复制用时:" + (end - start));
}

12.4 缓冲流(处理流之一)

作用:提高流的读取、写入速度

由于缓冲流是作用于节点流上的,只是将节点流读取的数据放入了一个缓存数组中,所以在缓冲流中有一个flush()方法来刷新缓冲区,但我们不用显示地调用flush来进行刷新,因为在源码中它已经调用此方法来进行自动刷新

12.4.1 字节流型

//经过测试,缓冲流对文件的复制的确比节点流的速度快
@Test
public void TestBuffer(){
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;
    try {
        //1.实例化File
        File srcFile = new File("E:\\桌面文件\\IOTest\\[乒乓球].ts");
        File destFile = new File("E:\\桌面文件\\IOTest\\[乒乓球缓冲流复制版].ts");
        //2.实例化流对象:先实例化内层的流,再实例化外层的流
        //2.1 实例化节点流
        FileInputStream fileInputStream = new FileInputStream(srcFile);
        FileOutputStream fileOutputStream = new FileOutputStream(destFile);
        //2.2 实例化缓冲流
        bis = new BufferedInputStream(fileInputStream);
        bos = new BufferedOutputStream(fileOutputStream);
        //3.文件的赋值
        byte[] buffer = new byte[1024];
        int len;
        while ((len = bis.read(buffer)) != -1){
            bos.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.资源关闭
        //要求:先关闭外层的流,再关闭内层的流
        //说明:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略
        try {
            if (bis != null)
                bis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (bos != null)
                bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.4.2 字符流型

@Test
public void TestBufferReaderWriter(){
    BufferedReader br = null;
    BufferedWriter bw = null;
    try {
        //1.实例化流和文件对象
        br = new BufferedReader(new FileReader(new File("hello1.txt")));
        bw = new BufferedWriter(new FileWriter(new File("hello2.txt")));
        //2.复制文件
        //方式一:使用char[]数组
        //char[] cbuf = new char[1024];
        //int len;
        //while ((len = br.read(cbuf)) != -1){
        //    bw.write(cbuf,0,len);
        //   //bw.flush();
        //}
        //方式二:使用String
        String data;
        while ((data = br.readLine()) != null){
            bw.write(data + "\n");//data不包含换行符
            //也可以调用bw.newLine()进行换行
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //3.关闭资源
        try {
            if (br != null)
                br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (bw != null)
                bw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.4.3 缓冲流练习

1.分别使用节点流:FilelnputStream、FileOutputStream和缓冲流:BufferedInputStream、BufferedOutputStream实现文本/文件/图片/视频文件的复制。并比较二者在数据复制方面的效率
2.实现图片加密操作。
提示:
int b = 0;
while((b = fis.read()) != -1){
    fos.write(b ^ 5);
}
3.获取文本上每个字符出现的次数
提示:遍历文本的每一个字符;字符及出现的次数保存在Map中;将Map中数据写入文件

12.5 转换流(处理流之二)

转换流提供了在字节流和字符流之间的转换

Java API提供了两个转换流:

  • lnputStreamReader:将lnputStream转换为Reader
  • OutputStreamWriter:将Writer转换为OutputStream

字节流中的数据都是字符时,转成字符流操作更高效

很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能

12.5.1 InputStreamReader

//处理流之二:转换流的使用
//1.转换流:属于字符流
//  InputStreamReader:将一个字节的输入流转换为字符的输入流
//  OutputStreamWriter:将一个字符的输出流转换为字节的输出流
//2.作用:提供字节流与字符流之间的转换
//3.解码:字节、字节数组--->字符数组、字符串
//  编码:字符数组、字符串--->字节、字节数组
//4.字符集
@Test
public void TestInputStreamReader(){
    InputStreamReader inputStreamReader = null;
    try {
        FileInputStream fileInputStream = new FileInputStream("hello1.txt");
        //参数2指明了字符集,具体使用哪个字符集,取决于读入文件保存时使用的字符集
        //InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);//使用系统默认的字符集
        inputStreamReader = new InputStreamReader(fileInputStream,"UTF-8");
        char[] cbuf = new char[20];
        int len;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            String str = new String(cbuf,0,len);
            System.out.println(str);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (inputStreamReader != null)
                inputStreamReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.5.2 OutputStreamWriter

//InputStreamReader和OutputStreamWriter实现文件的读入和写出
//文件以UTF-8格式读入,以GBK形式写出
@Test
public void TestOutputStreamWriter(){
    InputStreamReader inputStreamReader = null;
    OutputStreamWriter outputStreamWriter = null;
    try {
        FileInputStream fileInputStream = new FileInputStream(new File("hello2(UTF-8).txt"));
        FileOutputStream fileOutputStream = new FileOutputStream(new File("hello2(GBK).txt"));
        inputStreamReader = new InputStreamReader(fileInputStream,"UTF-8");
        outputStreamWriter = new OutputStreamWriter(fileOutputStream,"GBK");
        //数据读写
        char[] cbuf = new char[1024];
        int len;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            outputStreamWriter.write(cbuf,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if(inputStreamReader != null)
                inputStreamReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (outputStreamWriter != null)
                outputStreamWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.5.3 多种字符编码集的说明

编码表的由来

计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表

常见的编码表

  • ASCII:美国标准信息交换码。用一个字节的7位可以表示
  • ISO8859-1:拉丁码表。欧洲码表。用一个字节的8位表示
  • GB2312:中国的中文编码表最多两个字节编码所有字符
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
  • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示
  • UTF-8:变长的编码方式可用1-4个字节来表示一个字符

Unicode不完美,这里就有三个问题,一个是,我们已经知道,英文字母只用一个字节表示就够了,第二个问题是如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时间内无法推广,直到互联网的出现

面向传输的众多UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了

Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案推荐的Unicode编码是UTF-8和UTF-16

说明:在标准UTF-8编码中,超出基本多语言范围(BMP-Basic Multilinqual Plane)的字符被编码为4字节格式,但在修正的UTF-8编码中,他们由代理编码对( surrogatepairs )表示,然后这些代理编码对在序列中分别重新编码。结果标准UTF-8编码中需要4个字节的字符,在修正后的UTF-8编码中将需要6个字节

12.6 标准输入、输出流(处理流之三)

System.in和System.out分别代表了系统标准的输入和输出设备

默认输入设备是:键盘,输出设备是:显示器

System.in的类型是InputStreamSystem.out的类型是PrintStream,其是OutputStream的子类FilterOutputStream的子类

重定向:通过System类的setIn,setOut方法对默认设备进行改变

  • public static void setln(InputStream in)
  • public static void setOut(PrintStream out)
//1.标准的输入、输出流
//system.in:标准的输入流,默认从键盘输入
//system.out:标准的输出流,默认从控制台输出
public static void main(String[] args) {
    //从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序
    //方法一:使用Scanner,调用next()
    //方法二:使用System.in。System.in --> 转换流 --> BufferReader的readLine()
    BufferedReader bufferedReader = null;//将节点流的字符流进行包装,变成缓冲流
    try {
        InputStreamReader inputStreamReader = new InputStreamReader(System.in);//将标准输入流转换成字符流
        bufferedReader = new BufferedReader(inputStreamReader);//将字符流传入得到一个缓冲字符流
        while (true){
            System.out.print("请输入数据:");
            String data = bufferedReader.readLine();//调用缓冲字符流的readLine()方法读取一行数据
            if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)){//判断是否退出
                System.out.println("程序结束");
                break;
            }
            String upperData = data.toUpperCase();//将数据转换成大写
            System.out.println(upperData);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (bufferedReader != null)
                bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.7 打印流(处理流之四)

实现将基本数据类型的数据格式转化为字符串输出

打印流(只有输出流):PrintStream和PrintWriter

  • 提供了一系列重载的print()和println()方法,用于多种数据类型的输出
  • PrintStream和PrintWriter的输出不会抛出lOException异常
  • PrintStream和PrintWriter有自动flush功能
  • PrintStream打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用PrintWriter 类
  • System.out返回的是PrintStream的实例
@Test
public void TestPrintStreamWriter(){
    PrintStream ps = null;
    try {
        FileOutputStream fos = new FileOutputStream(new File("hello3.txt"));
        //创建打印输出流,设置为自动刷新模式(写入换行符或字节‘\n’时都会刷新输出缓冲区)
        ps = new PrintStream(fos,true);
        if (ps != null) {//把标准输出流(控制台输出)改成文件
            System.setOut(ps);
        }
        for (int i = 0; i <= 255; i++) { //输出ASCII字符
            System.out.print((char) i);
            if (i % 50 == 0) {//每50个数据一行
                System.out.println(); //换行
            }
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (ps != null)
            ps.close();
    }
}

12.8 数据流(处理流之五)

为了方便地操作Java语言的基本数据类型和String的数据,可以使用数据流

数据流有两个类:(用于读取和写出基本数据类型、String类的数据)

  • DatalnputStream和 DataOutputStream
  • 分别“套接”在 InputStream和l OutputStream子类的流上

DatalnputStream中的方法:

  • boolean readBoolean()
  • byte readByte()
  • char readChar()
  • float readFloat()
  • double readDouble()
  • short readShort()
  • long readLong()
  • int readlnt()
  • String readUTF()
  • void readFully(byte[] b)

DataOutputStream中的方法:将上述的方法的read改为相应的write即可

@Test
public void TestDataOutputStream(){
    //将内存中的字符串、基本数据类型的变量写出到文件夹中
    DataOutputStream dataOutputStream = null;
    try {
        dataOutputStream = new DataOutputStream(new FileOutputStream("data.txt"));
        dataOutputStream.writeUTF("张三");
        dataOutputStream.flush();//刷新操作,将内存中的数据写入文件
        dataOutputStream.writeInt(23);
        dataOutputStream.flush();
        dataOutputStream.writeBoolean(true);
        dataOutputStream.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (dataOutputStream != null)
                dataOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Test
public void TestDataInputStream() {
    //将文件中的字符串、基本数据类型数据读取到到内存中
    //注意点:读取文件数据的顺序必须和保存数据的顺序一致
    DataInputStream dataInputStream = null;
    try {
        dataInputStream = new DataInputStream(new FileInputStream("data.txt"));
        String name = dataInputStream.readUTF();
        int age = dataInputStream.readInt();
        boolean sex = dataInputStream.readBoolean();
        System.out.println("name = " + name);
        System.out.println("age = " + age);
        System.out.println("sex" + sex);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (dataInputStream != null)
                dataInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.9 对象流(处理流之六)

ObjectInputStream和OjbectOutputSteam:用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来

序列化:用ObjectOutputStream类保存基本类型数据或对象的机制

反序列化:用ObjectInputStream类读取基本类型数据或对象的机制

ObjectOutputStream和ObjectInputStream不能序列化static和ltransient修饰的成员变量

12.9.1 对象的序列化

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象

序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原

序列化是RMI(Remote Method lnvoke - 远程方法调用)过程的参数和返回值都必须实现的机制,而RMI是JavaEE的基础。因此序列化机制是JavaEE 平台的基础

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotSerializableException异常

  • Serializable
  • Externalizable

12.9.2 测试ObjectOutputStream和ObjectInputStream

@Test
public void TestObjectOutputStream() {
    //序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出出去:使用ObjectOutputStream实现
    ObjectOutputStream objectOutputStream = null;
    try {
        objectOutputStream = new ObjectOutputStream(new FileOutputStream("Object.dat"));
        objectOutputStream.writeObject(new String("我爱北京天安门"));
        objectOutputStream.flush();//刷新操作
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if(objectOutputStream != null)
                objectOutputStream.close();//关闭
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Test
public void TestObjectInputStream(){
    //反序列化过程:将保存到磁盘文件中的对象还原为内存中的java对象:使用ObjectInputStream实现
    ObjectInputStream objectInputStream = null;
    try {
        objectInputStream = new ObjectInputStream(new FileInputStream("Object.dat"));
        Object o = objectInputStream.readObject();
        String str = (String) o;
        System.out.println(str);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        if(objectInputStream != null) {
            try {
                objectInputStream.close();//关闭
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

12.9.3 自定义类实现序列化和反序列化操作

自定义类能被序列化的要求:

  1. 自定义类需要实现Serializable接口和Externalizable接口之一
  2. 当前类提供一个全局常量: serialVersionUID
  3. 除了当前Person类需要实现Serializable接口之外,还必须保证其内部所有属性(属性的类型也实现接口)也必须是可序列化的(默认情况下,基本数据类型都可序列化)
public class Person implements Serializable {
    public static final long serialVersionUID = 79275029514L;//随便赋值,这是一个标识
    private String name;
    private int age;
    public Person() {
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
@Test
public void TestCustomizeObject(){
    //将java对象序列化
    ObjectOutputStream objectOutputStream = null;
    try {
        objectOutputStream = new ObjectOutputStream(new FileOutputStream("Object.dat"));
        objectOutputStream.writeObject(new Person("张三",20));
        objectOutputStream.flush();//刷新操作
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if(objectOutputStream != null)
                objectOutputStream.close();//关闭
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Test
public void TestCustomizeObject(){
    //将java对象反序列化
    ObjectInputStream objectInputStream = null;
        try {
            objectInputStream = new ObjectInputStream(new FileInputStream("Object.dat"));
            Object obj = objectInputStream.readObject();
            Person p = (Person) obj;
            System.out.println(p);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
}

12.9.4 序列化时serialVersionUID的理解

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:private static final long serialVersionUID

  • serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容
  • 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID可能发生变化。故建议,显式声明

简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)

(自己的理解)首先假设Person类的serialVersionUID被显式声明,此时对Person类的person对象进行序列化,然后我们对这个Person类内部进行一些比如属性个数增加,方法增多,属性值变化等的修改,然后我们对对象person进行反序列化,此时由于serialVersionUID的显式声明,即被唯一确定,那么这个对象依然能携带着被修改的部分被还原;再假设Person类的serialVersionUID没有被显式声明,则person对象在进行序列化时Java运行环境会自动生成一个serialVersionUID,但当我们做了一些修改后,Java运行环境为Person类自动生成的serialVersionUID就可能会发生变化,那么我们进行反序列化时,JVM就会因为serialVersionUID的不同无法判断被序列化的对象person是哪个类的对象,从而就无法还原

12.10 随机存取文件流

RandomAccessFile声明在java.io包下,但直接继承于java.lang.Object类。并且它实现了Datalnput、DataOutput这两个接口,也就意味着这个类既可以读也可以写

RandomAccessFile类支持“随机访问”的方式,程序可以直接跳到文件的任意地方来读、写文件

  • 支持只访问文件的部分内容
  • 可以向已存在的文件后追加内容

RandomAccessFile对象包含一个记录指针,用以标示当前读写处的位置

RandomAccessFile 类对象可以自由移动记录指针

  • long getFilePointer():获取文件记录指针的当前位置
  • void seek(long pos):将文件记录指针定位到pos位置

构造器:

  • public RandomAccessFile(File file, String mode)
  • public RandomAccessFile(String name, String mode)

创建RandomAccessFile类实例需要指定一个mode参数,该参数指定RandomAccessFile的访问模式

  • r:以只读方式打开
  • rw:打开以便读取和写入
  • rwd:打开以便读取和写入;同步文件内容的更新
  • rws:打开以便读取和写入;同步文件内容和元数据的更新

如果模式为只读r,则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常

如果模式为rw读写。如果文件不存在则会去创建文件;如果存在则不会创建,但是会对原有文件的内容进行覆盖(默认从头覆盖)

补充:JDK1.6上面写的每次write数据时,**”rw”模式,数据不会立即写到硬盘中;而“rwd”,数据会被立即写入硬盘。如果写数据过程发生异常,“rwd”模式中已被wite的数据被保存到硬盘,而“rw”则全部丢失**

12.10.1 RandomAccessFile的使用

1.RandomAccessFile对文件数据的读取和写入
//RandomAccessFiLe的使用
//1.RandomAccessFile直接继承于java.Lang.object类,实现了DataInput和DataOutput接口
//2.RandomAccessFile既可以作为一个输入流,又可以作为一个输出流
@Test
public void TestRandomAccessFile(){
    //对数据进行读取和写入(复制)
    RandomAccessFile randomAccessFile1 = null;
    RandomAccessFile randomAccessFile2 = null;
    try {
        randomAccessFile1 = new RandomAccessFile(new File("TestImage.jpg"),"r");
        randomAccessFile2 = new RandomAccessFile(new File("TestImage2.jpg"),"rw");

        byte[] buffer = new byte[1024];
        int len;
        while ((len = randomAccessFile1.read(buffer)) != -1){
            randomAccessFile2.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (randomAccessFile1 != null) {
            try {
                randomAccessFile1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (randomAccessFile2 != null) {
            try {
                randomAccessFile2.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
2.RandomAccessFile对文件原有数据的覆盖效果
@Test
public void Test(){
    //向文件添加新数据时默认从头开始覆盖原有内容
    RandomAccessFile randomAccessFile = null;
    try {
        randomAccessFile = new RandomAccessFile(new File("hello.txt"),"rw");
        randomAccessFile.write("xyz".getBytes());//write展示出的效果是对原有内容的覆盖
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (randomAccessFile != null)
                randomAccessFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
3.RandomAccessFile实现对文件数据的插入
//实现数据的插入
@Test
public void TestRandomAccessFileInsert() throws FileNotFoundException {
    RandomAccessFile randomAccessFile = null;
    try {
        randomAccessFile = new RandomAccessFile(new File("hello.txt"),"rw");
        randomAccessFile.seek(3);//将角标移动到3的位置(角标从0开始)
        //将指针3后面的数据内容保存到StringBuilder中
        StringBuilder builder = new StringBuilder((int) new File("hello.txt").length());
        byte[] buffer = new byte[20];
        int len;
        while ((len = randomAccessFile.read(buffer)) != -1){
            builder.append(new String(buffer,0,len));
        }
        //将指针重新定位到3处
        randomAccessFile.seek(3);
        randomAccessFile.write("abc".getBytes());
        //将StringBuilder中数据写入文件中
        randomAccessFile.write(builder.toString().getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (randomAccessFile != null)
                randomAccessFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
//思考:将StringBuilder替换为ByteArrayOutputStream

12.11 NIO.2中Path、Paths、Files类的使用

12.11.1 NIO的介绍

Java NlO (New lO,Non-Blocking lO)是从Java 1.4版本开始引入的一套新的IO APl,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作

JavaAPI中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO

  • java.nio.channels.Channel
    • FileChannel:处理本地文件
    • SocketChannel:TCP网络编程的客户端的Channel
    • ServerSocketChannel:TCP网络编程的服务器端的Channel
    • DatagramChannel:UDP网络编程中发送端和接收端的Channel

随着JDK7的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为NIO.2。因为NIO提供的一些功能,NIO已经成为文件处理中越来越重要的部分

12.11.2 Path、Paths、Files核心API

早期的Java只提供了一个File类来访问文件系统,但File类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息

NIO. 2为了弥补这种不足,引入了Path接口, 代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path可以看成是File类的升级版本, 实际引用的资源也可以不存在

在以前IO操作都是这样写的:
import java.io.File;
File file = new File("index.html");
但在Java7中,可以这样写:
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("index.html");

同时,NIO.2在java.nio.file包下还提供了Files、Paths 工具类,Files包含了大量静态的工具方法来操作文件; Paths则包含了两个返回Path的静态工厂方法

Paths类提供的静态get()方法用来获取Path对象:

  • static Path get(String first, String…more):用于将多个字符串串连成路径
  • static Path get(URI uri):返回指定uri对应的Path路径
1.Path接口

Path常用方法:

  • String toString():返回调用Path对象的字符串表示形式
  • boolean startsWith(String path):判断是否以path路径开始
  • boolean endsWith(String path):判断是否以path路径结束
  • boolean isAbsolute():判断是否是绝对路径
  • Path getParent():返回Path对象包含整个路径,不包含Path对象指定的文件路径
  • Path getRoot():返回调用Path对象的根路径
  • Path getFileName():返回与调用Path对象关联的文件名
  • int getNameCount():返回Path根目录后面元素的数量
  • Path getName(int idx):返回指定索引位置idx的路径名称
  • Path toAbsolutePath():作为绝对路径返回调用Path 对象
  • Path resolve(Path p):合并两个路径,返回合并后的路径对应的Path对象
  • File toFile():将Path转化为File类的对象
2.Files类

java.nio.file.Files用于操作文件或目录的工具类

Files常用方法:

  • Path copy(Path src, Path dest, CopyOption…how):文件的复制
  • Path createDirectory(Path path, FileAttribute<?>…attr):创建一个目录
  • Path createFile(Path path, FileAttribute<?>…arr):创建一个文件
  • void delete(Path path):删除一个文件/目录,如果不存在,执行报错
  • void deletelfExists(Path path):Path对应的文件/目录如果存在,执行删除
  • Path move(Path src, Path dest, CopyOption…how):将src移动到dest位置
  • long size(Path path):返回path指定文件的大小

用于判断:

  • boolean exists(Path path, LinkOption…opts):判断文件是否存在

  • boolean isDirectory(Path path, LinkOption…opts):判断是否是目录

  • boolean isRegularFile(Path path, LinkOption…opts):判断是否是文件

  • boolean isHidden(Path path):判断是否是隐藏文件

  • boolean isReadable(Path path):判断文件是否可读

  • boolean isWritable(Path path):判断文件是否可写

  • boolean notExists(Path path, LinkOption…opts):判断文件是否不存在

用于操作内容:

  • SeekableByteChannel newByteChannel(Path path, OpenOption…how):获取与指定文件的连接,how指定打开方式
  • DirectoryStream newDirectoryStream(Path path):打开path指定的目录
  • InputStream newlnputStream(Path path, OpenOption… how):获取InputStream 对象
  • OutputStream newOutputStream(Path path, OpenOptin… how):获取OutputStream对象

十三、Java网络编程

13.1 网络编程概述

Java是Internet上的语言,它从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的网络应用程序

Java提供的网络类库,可以实现无痛(不用关注底层)的网络连接,联网的底层细节被隐藏在Java的本机安装系统里,由JVM进行控制。并且Java实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境

13.1.1 网络基础

计算机网络:把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、共享硬件、软件、数据信息等资源

网络编程的目的:直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯

网络编程中有两个主要的问题:①如何准确地定位网络上一台或多台主机;定位主机上的特定的应用找到主机后如何可靠高效地进行数据传输

13.1.2 如何实现网络中的主机互相通信(网络编程的两个要素)

通信双方地址

  • IP
  • 端口号

一定的规则(即:网络通信协议。有两套参考模型)

  • OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广
  • **TCP/IP参考模型(或TCP/IP协议)**:事实上的国际标准
1.TCP/IP参考模型各层的作用

应用层:提供应用程序的网络接口

传输层:两台主机间的应用程序提供端到端的通信。传输层从应用层接受数据,并且在必要的时候把它分成较小的单元传递给网络层,并确保到达对方的各段信息正确无误

网络层:主要协议有IP (Internet protocol) 、ICMP (Internet Control Message Protocol ,互联网控制报文协议)、IGMP ( Internet Group Management Protocol,互联网组管理协议)、ARP (Address Resolution Protocol ,地址解析协议)和RARP (Reverse Address Resolution Protocol ,反向地址解析协议)等。涉及寻址和路由选择

物理层和数据链路层:涉及物理介质访问和二进制数据流传输

2.各层涉及到的具体协议:

TCP/IP是基于TCP和IP这两个最初的协议之上的不同的通信协议的大集合。又称为TCP/IP协议簇

协议名称 协议作用
TCP-传输控制协议 TCP用于从应用程序到网络的数据传输控制
TCP负责在数据传送之前将它们分割为IP包,然后在它们到达的时候将它们重组
IP-网际协议 IP负责计算机之间的通信
IP负责在因特网上发送和接收数据包
HTTP-超文本传输协议 HTTP负责web服务器与web浏览器之间间的通信
HTTP用于从web客户端(浏览器)与web服务器发送请求,并从web服务器向web客户端返回内容(网页)
HTTPS-安全的HTTP HTTPS负责在web服务器和web浏览器之间的安全通信
作为有代表性的应用, HTTPS会用于处理信用卡交易和其他的敏感数据
SSL-安全套接字层 SSL协议用于为安全数据传输加密数据
SMTP-简易邮件传输协议 SMTP用于电子邮件的传输
MIME-多用途因特网邮件扩展 MIME协议使SMTP有能力通过TCP/IP网络传输多媒体文件 ,包括声音、 视频和二进制数据
IMAP-因特网消息访问协议 IMAP用于存储和取回电子邮件
POP-邮局协议 POP用于从电子邮件服务器向个人电脑下载电子邮件
FTP-文件传输协议 FTP负责计算机之间的文件传输
NTP-网络时间协议 NTP用于在计算机之间同步时间(钟)
DHCP-动态主机配置协议 DHCP用于向网络中的计算机分配动态IP地址
SNMP-简单网络管理协议 SNMP用于计算机网络的管理
LDAP-轻量级的目录访问协议 LDAP用于从因特网搜集关于用户和电子邮件地址的信息
ICMP-因特网消息控制协议 ICMP负责网络中的错误处理
ARP-Address Resolution Protocol ARP-用于通过IP来查找基于IP地址的计算机网卡的硬件地址
BOOTP-Boot Protocol BOOTP用于从网络启动计算机
PPTP-点对点隧道协议 PPTP用于私人网络之间的连接(隧道)
3.网络通信示例图

13.2 通信要素1:IP和端口号

13.2.1 IP地址

IP地址:InetAddress(Java中一个InetAddress对象就代表一个IP地址)

  • 唯一的标识Internet上的计算机(通信实体)
  • 本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
  • IP地址分类方式1: IPV4 和IPV6
    • IPV4:4个字节组成,4个0-255。 大概42亿,30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如192.168.0.1
    • VIPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开,如: 3ffe:3201:1401:1280:c8ff.fe4d:db39:1984
  • IP地址分类方式2:**公网地址(万维网使用)私有地址(局域网使用)**。192.168.开头的就是私有址址,范围即为192.168.0.0——192.168.255.255,专门为组织机构内部使用
  • 特点:不易记忆
1.通过域名访问对应的IP地址的网址
2.InetAddress类的使用
//如何实例化InetAddress:两个方法getByName(String host)、getLocalHost()
//两个常用方法:getHostName()、getHostAddress()
InetAddress address = InetAddress.getByName("192.168.9.59");//通过字符串生成一个IP地址
System.out.println(address);///192.168.9.59
InetAddress address1 = InetAddress.getByName("www.baidu.com");//通过域名然后内部解析后生成一个IP地址
System.out.println(address1);//www.baidu.com/14.215.177.39
System.out.println(address1.getHostName());//获取主机名字
System.out.println(address1.getHostAddress());//获取主机地址

InetAddress address2 = InetAddress.getByName("127.0.0.1");//通过字符串获取本机IP地址
System.out.println(address2);
InetAddress address3 = InetAddress.getLocalHost();//另一种获取本机IP地址的方法
System.out.println(address3);

13.2.2 端口号

端口号标识正在计算机上运行的进程(程序)

  • 不同的进程有不同的端口号
  • 被规定为一个16位的整数0~65535
  • 端口分类:
    • 公认端口:0~1023。 被预先定义的服务通信占用(如: HTTP占用端口
      80,FTP占用端口21,Telnet占用端口23)
    • 注册端口:1024~49151分配给用户进程或应用程序。( 如: Tomcat占
      用端口8080,MySQL占用端口3306,Oracle 占用端口1521等)
    • 动态/私有端口:49152~65535

端口号与IP地址的组合得出一个网络套接字:Socket

1.两台主机间同一应用的通信:

13.3 通信要素2:网络通信协议

网络通信协议:计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准

计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量控制,路由控制,这就产生了如何实现如此复杂的网络协议的问题

通信协议分层的思想:在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一层不发生关系各层互不影响,利于系统的开发和扩展

13.3.1 TCP/IP协议簇

涉及到传输层和网络层的协议

传输层协议中有两个非常重要的协议:

  • 传输控制协议TCP(Transmission Control Protocol)
  • 用户数据报协议UDP(User Datagram Protocol)

TCP/IP以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议

  • IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信
  • TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、IP层、传输层和应用层

13.3.2 传输层的TCP和UDP协议主要区别

TCP协议:

  • 使用TCP协议前,须先建立TCP连接,形成传输数据通道
  • 传输前,采用“三次握手”方式,点对点通信,是可靠的
  • TCP协议进行通信的两个应用进程:客户端、服务端
  • 在连接中可进行大数据量的传输
  • 传输完毕,需释放已建立的连接,效率低

UDP协议:

  • 将数据、源、目的封装成数据包,不需要建立连接
  • 每个数据报的大小限制在64K内
  • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
  • 可以广播发送
  • 发送数据结束时无需释放资源,开销小,速度快

TCP生活案例:打电话
UDP生活案例:发送短信、发电报

1.TCP三次握手和四次挥手

第一次握手:建立连接时,客户端发送SYN包(seq=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)

第二次握手:服务器收到SYN包,必须确认客户端的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手

客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close0操作即可产生挥手操作

(1)客户端A发送个FIN,用来关闭客户A到服务器B的数据传送

(2)服务器B收到这个FIN,它发回个ACK,确认序号为收到序号加1和SYN一样,一个FIN将占用一个序号

(3)服务器B关闭与客户端A的连接,发送个FIN给客户端A

(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1

13.4 TCP网络编程

13.4.1 实例1:客户端发送信息给服务端,服务端将信息显示在控制台

//实现TCP网络编程
//实例1:客户端发送信息给服务端,服务端将信息显示在控制台
//测试程序时要先启动服务器端等待客户端呼叫
@Test
public void Client(){//客户端
    Socket socket = null;
    OutputStream outputStream = null;
    try {
        //1.创建IP地址对象
        InetAddress address = InetAddress.getByName("127.0.0.1");//服务器IP地址,由于是自己测试,所以用本机IP
        //2.创建Socket对象,指明服务器端的ip和端口号
        socket = new Socket(address,8899);//利用要传输到的IP地址和传输到的端口号创建一个套接字Socket对象
        //3.获取一个输出流,用于输出数据
        outputStream = socket.getOutputStream();//通过这个对象获取一个输出流
        //4.输出数据
        outputStream.write("你好,我是客户端".getBytes());//输出内容到服务端
    } catch (IOException e) {
        e.printStackTrace();
    } finally {//5.关闭资源
        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
@Test
public void Server(){//服务器端
    ServerSocket serverSocket = null;
    Socket socket = null;
    InputStream inputStream = null;
    ByteArrayOutputStream byteArrayOutputStream = null;
    try {
        //1.创建ServerSocket对象,指明自己的端口号
        serverSocket = new ServerSocket(8899);
        //2.调用accept()方法来接收客户端传输过来的socket
        socket = serverSocket.accept();
        //3.获取一个输入流来读取数据
        inputStream = socket.getInputStream();
        //这样写可能会乱码,如果传过来的信息的字节流因为buffer数组的容量不够大,读取的时候被拆散开,那么在转换成字符串的时候就可能会出现乱码
        //比如客户端传过来一段中文,buffer数组的容量又为5,即一次性只能读取5个字节,而一个中文字符如果占3个字节,
        //那么第二个中文字符的字节就会被拆散开,在另一部分字节被读取之前,我们又需要把已读取的字节转换成字符串输出出来,这时候就可能会出现乱码
        //byte[] buffer = new byte[20];
        //int len;
        //while ((len = inputStream.read(buffer)) != -1){
        //    String str = new String(buffer,0,len);
        //    System.out.println(str);
        //}
        //4.读取输入流中数据
        byteArrayOutputStream = new ByteArrayOutputStream();
        //5.对数据进行操作
        byte[] buffer = new byte[5];
        int len;
        while ((len = inputStream.read(buffer)) != -1){
            byteArrayOutputStream.write(buffer,0,len);
        }
        System.out.println(byteArrayOutputStream.toString());
        System.out.println("收到来自" + socket.getInetAddress().getHostAddress() + "的消息");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {//6.关闭资源
        if ((socket != null)) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (byteArrayOutputStream != null) {
            try {
                byteArrayOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.4.2 实例2:客户端发送文件给服务端,服务端将文件保存在本地

//实例2:客户端发送文件给服务端,服务端将文件保存在本地
@Test
public void Client(){//客户端
    Socket socket = null;
    OutputStream outputStream = null;
    FileInputStream fileInputStream = null;
    try {
        //1.创建IP地址对象
        InetAddress address = InetAddress.getByName("127.0.0.1");
        //2.创建Socket对象
        socket = new Socket(address,9090);
        //3.获取输出流,用于输出数据到服务器端
        outputStream = socket.getOutputStream();
        //4.创建一个文件字节输入流,读取文件到数组buffer中让输出流输出
        fileInputStream = new FileInputStream(new File("TestImage.jpg"));
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fileInputStream.read(buffer)) != -1){
            outputStream.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {//5.关闭资源
        try {
            if (fileInputStream != null)
                fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (outputStream != null)
                outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (socket != null)
                socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Test
public void Server(){//服务器端
    ServerSocket serverSocket = null;
    Socket socket = null;
    InputStream inputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        //1.创建ServerSocket对象
        serverSocket = new ServerSocket(9090);
        //2.调用ServerSocket的accept()方法获取客户端的套接字对象以进行通信
        socket = serverSocket.accept();
        //3.获取一个输入流来读取客户端的数据
        inputStream = socket.getInputStream();
        //4.创建一个文件字节输出流,把客户端的文件保存到本地
        fileOutputStream = new FileOutputStream(new File("SocketImage.jpg"));
        //5.用输入流读取客户端的数据,再让文件字节输出流输出数据保存到本地
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) != -1){
            fileOutputStream.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileOutputStream != null)
                fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (inputStream != null)
                inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (socket != null)
                socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (serverSocket != null)
                serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

13.4.3 实例3:从客户端发送文件给服务端,服务端保存到本地,并返回“发送成功”给客户端,并关闭相应的连接

//3.从客户端发送文件给服务端,服务端保存到本地,并返回“发送成功”给客户端,并关闭相应的连接
@Test
public void Client(){
    Socket socket = null;
    OutputStream outputStream = null;
    FileInputStream fileInputStream = null;
    InputStream inputStream = null;
    ByteArrayOutputStream byteArrayOutputStream = null;
    try {
        //1.创建IP地址对象
        InetAddress address = InetAddress.getByName("127.0.0.1");
        //2.创建Socket对象
        socket = new Socket(address,9090);
        //3.获取输出流,用于输出数据到服务器端
        outputStream = socket.getOutputStream();
        //4.创建一个文件字节输入流,读取文件到数组buffer中让输出流输出
        fileInputStream = new FileInputStream(new File("TestImage.jpg"));
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fileInputStream.read(buffer)) != -1){
            outputStream.write(buffer,0,len);
        }
        //注意,此处需要利用socket对象关闭输出流来告诉服务器端传输已完成,否则服务器端就会一直等待客户端的下一次传输而不执行下方的反馈代码
        socket.shutdownOutput();
        //5.获取一个输入流,用于读取来自服务器端的数据
        inputStream = socket.getInputStream();
        //6.创建字节数组输出流,用于将输入流读取的服务器端数据输出
        byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer1 = new byte[20];//存储读取到的数据
        int len1;
        while ((len1 = inputStream.read(buffer1)) != -1){
            byteArrayOutputStream.write(buffer1,0,len1);//将读取到的数据写到自身类定义的数组中
        }
        System.out.println(byteArrayOutputStream.toString());//将数组转换成字符串输出到控制台
    } catch (IOException e) {
        e.printStackTrace();
    } finally {//7.关闭资源
        try {
            if (fileInputStream != null)
                fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (outputStream != null)
                outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (socket != null)
                socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (inputStream != null)
                inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (byteArrayOutputStream != null)
                byteArrayOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Test
public void Server(){
    ServerSocket serverSocket = null;
    Socket socket = null;
    InputStream inputStream = null;
    FileOutputStream fileOutputStream = null;
    OutputStream outputStream = null;
    try {
        //1.创建ServerSocket对象
        serverSocket = new ServerSocket(9090);
        //2.调用ServerSocket的accept()方法获取客户端的套接字对象以进行通信
        socket = serverSocket.accept();
        //3.获取一个输入流来读取客户端的数据
        inputStream = socket.getInputStream();
        //4.创建一个文件字节输出流,把客户端的文件保存到本地
        fileOutputStream = new FileOutputStream(new File("SocketImage1.jpg"));
        //5.用输入流读取客户端的数据,再让文件字节输出流输出数据保存到本地
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) != -1){
            fileOutputStream.write(buffer,0,len);
        }
        System.out.println("接收数据完成");
        //6.获取一个输出流对象,用于给客户端反馈
        outputStream = socket.getOutputStream();
        outputStream.write("服务器端已收到文件".getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {//7.关闭资源
        try {
            if (fileOutputStream != null)
                fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (inputStream != null)
                inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (socket != null)
                socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (serverSocket != null)
                serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (outputStream != null)
                outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

13.4.4 实例4:服务端读取图片并发送给客户端,客户端保存图片到本地

13.4.5 实例5:客户端给服务端发送文本,服务端会将文本转成大写在返回给客户端

13.5 UDP网络编程

类 DatagramSocket和DatagramPacket实现了基于UDP协议网络程序

UDP数据报通过数据报套接字DatagramSocket发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达

DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号

UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样

//UDP网络编程
//UDP要传输的数据、传输到的主机IP及端口都打包在数据报中,所以socket只是一个根据数据报中的IP和端口进行发送数据报和接收数据报的作用
@Test
public void Send() throws IOException {//发送端
    //1.创建数据报套接字DatagramSocket对象,用于传输数据
    DatagramSocket socket = new DatagramSocket();
    String str = "我是UDP发送端";//要传输的数据
    byte[] data = str.getBytes();
    //2.获取要发送到的IP地址对象
    InetAddress address = InetAddress.getByName("127.0.0.1");
    //3.创建发送端的数据报对象,其中包含要传输的数据、要传输到的主机的IP地址及端口
    DatagramPacket packet = new DatagramPacket(data,0,data.length,address,9090);
    //4.发送打包好的数据报
    socket.send(packet);
    //5.关闭socket
    socket.close();
}
@Test
public void Receiver() throws IOException {//接收端
    //1.创建数据报套接字DatagramSocket对象,用于接收传入的端口参数中的数据
    DatagramSocket socket = new DatagramSocket(9090);
    byte[] buffer = new byte[100];//存储数据的数组
    //2.创建接收端的数据报对象,来接收保存传来的数据报
    DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
    //3.接收发送端的数据报
    socket.receive(packet);
    System.out.println(new String(packet.getData(),0,packet.getLength()));//对数据进行处理操作
    //4.关闭socket
    socket.close();
}

13.6 URL编程

URL(Uniform Resource Locator):统一资源定位符,它表示Internet上某一资源的地址

它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源

通过URL我们可以访问Internet上的各种网络资源,比如最常见的 www,ftp站点。浏览器通过解析给定的URL可以在网络上查找相应的文件或其他资源

URL的基本结构由5部分组成:
<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表>

例如:http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123>

#片段名:即锚点,例如看小说,直接定位到章节

参数列表格式:参数名=参数值&参数名=参数值…

13.6.1 构造器

为了表示URL,java.net中实现了类URL。我们可以通过下面的构造器来初始化一个URL对象:

  • public URL(String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例如:URL url = new URL ("http://www.baidu.com");
  • public URL(URL context,String spec):通过基URL和相对URL构造一个URL对象。例如:URL downloadUrl = new URL(url, "download.html");
  • public URL(String protocol, String host, String file);例如:new URL("http","www.baidu.com","download.html");
  • public URL(String protocol, String host, int port, String file);例如:URL gamelan = new URL("http", "www.baidu.com", 80, "download.html");

URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用try-catch语句进行捕获

13.6.2 常用方法

一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:

  • public String getProtocol( ):获取该URL的协议名
  • public String getHost( ):获取该URL的主机名
  • public String getPort( ):获取该URL的端口号
  • public String getPath( ):获取该URL的文件路径
  • public String getFile( ):获取该URL的文件名
  • public String getQuery( ):获取该URL的查询名
//URL网络编程
//1.URL:统—资源定位符,对应着互联网的某—资源地址2.格式:
//http://Localhost:8080/TestImage.jpg?username=Tom
// 协议     主机名   端口号  资源地址        参数列表
@Test
public void TestURL(){
    URL url = null;
    try {
        url = new URL("http://Localhost:8080/examples/TestImage.jpg?username=Tom");
        //public String getProtocol():获取该URL的协议名
        System.out.println(url.getProtocol());// http
        //public String getHost():获取该URL的主机名
        System.out.println(url.getHost());// Localhost
        //public String getPort():获取该URL的端口号
        System.out.println(url.getPort());// 8080
        //public String getPath():获取该URL的文件路径
        System.out.println(url.getPath());// /TestImage.jpg
        //public String getFile():获取该URL的文件名
        System.out.println(url.getFile());// /TestImage.jpg?username=Tom
        //public String getQuery():获取该URL的查询名
        System.out.println(url.getQuery());// username=Tom
    } catch (MalformedURLException e) {
        e.printStackTrace();
    }
}
//URL实现Tomcat服务器端资源下载
@Test
public void TestURLDownload(){
    HttpURLConnection urlConnection = null;
    InputStream inputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        //1.创建一个URL对象,确定要访问的地址和资源
        URL url = new URL("http://Localhost:8080/examples/TestImage.jpg");
        //2.获取Http连接
        urlConnection = (HttpURLConnection) url.openConnection();//用于获取跟服务器的连接
        //3.连接到目的地址
        urlConnection.connect();
        //4.获取一个输入流对象,用来读入资源
        inputStream = urlConnection.getInputStream();
        //5.创建一个文件字节输出流对象,用来将输入流读取的资源保存到本地
        fileOutputStream = new FileOutputStream("URLImage.jpg");
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) != -1){
            fileOutputStream.write(buffer,0,len);
        }
        System.out.println("下载完成");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //6.关闭资源和连接
        try {
            if (fileOutputStream != null)
                fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (inputStream != null)
                inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (urlConnection != null)
            urlConnection.disconnect();
    }
}

十四、Java反射机制

14.1 Java反射机制概述

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射

正常方式:引入需要的“包类”名称 —> 通过new实例化 —> 取得实例化对象
反射方式:实例化对象 —> getClass()方法 —> 得到完整的“包类”名称

14.1.1 动态语言与静态语言

1、动态语言

是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构
主要动态语言:Object-C、C#、JavaSript、PHP、Python、Erlang

2、静态语言

与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++
Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性,Java的动态性让编程的时候更加灵活

14.1.2 Java反射机制研究及应用

Java反射机制提供的功能:

  • 运行时判断任意一个对象所属的类
  • 运行时构造任意一个类的对象
  • 运行时判断任意一个类所具有的成员变量和方法
  • 运行时获取泛型信息
  • 运行时调用任意一个对象的成员变量和方法
  • 运行时处理注解
  • 生成动态代理

14.1.3 反射相关的API

  • java.lang.Class:代表一个类(所有的类都相当于是Class的对象)
  • java.lang.reflect.Method:代表类的方法
  • java.lang.reflect.Field:代表类的成员变量
  • java.lang.reflect.Constructor:代表类的构造器
  • ……

14.1.4 反射前后对一个类的操作对比

//反射之前,对于Person类的操作
@Test
public void TestBeforeReflection(){
    //1.创建Person对象
    Person p1 = new Person("Tom",12);
    //2.通过对象,调用其内部的属性、方法
    p1.age = 10;
    System.out.println(p1.toString());
    p1.show();
    //在Person类外部,不可以通过Person类的对象调用其内部私有结构
    //比如,name、showNation()、Person(String name)
}
//使用反射之后
@Test
public void TestAfterReflection() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
    Class clazz = Person.class;
    //1.通过反射创建Person类的对象
    Constructor constructor = clazz.getConstructor(String.class, int.class);
    Object tom = constructor.newInstance("Tom", 12);
    Person p1 = (Person) tom;
    System.out.println(p1.toString());//Person{name='Tom', age=12}
    //2.通过反射,调用对象指定的属性、方法
    //调用属性
    Field age = clazz.getDeclaredField("age");
    age.set(p1,10);
    System.out.println(p1.toString());//Person{name='Tom', age=10}
    //调用方法
    Method show = clazz.getDeclaredMethod("show");
    show.invoke(p1);//这是Person类

    //通过反射,还可以调用Person类的私有结构的。比如:私有的构造器、方法、属性
    //私有构造器
    Constructor constructor1 = clazz.getDeclaredConstructor(String.class);
    constructor1.setAccessible(true);
    Person jerry = (Person) constructor1.newInstance("Jerry");
    System.out.println(jerry);//Person{name='Jerry', age=0}
    //私有属性
    Field name = clazz.getDeclaredField("name");
    name.setAccessible(true);
    name.set(jerry,"Mary");
    System.out.println(jerry);//Person{name='Mary', age=0}
    //私有方法
    Method showNation = clazz.getDeclaredMethod("showNation", String.class);
    showNation.setAccessible(true);
    String nation = (String) showNation.invoke(jerry,"中国");//相当于String nation = jerry.showNation("中国")   我的国籍是中国
    System.out.println(nation);//中国
}

14.2 Class类的理解及获取Class实例

14.2.1 Class类的理解

关于java.Lang.class类的理解:

1.类的加载过程:

程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class结尾)
接着我们使用java.exe命令对某个字节码文件进行解释运行相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类此运行时类,就作为Class的一个实例

比如,上方的Person类被编写成Person.class文件还不是一个运行时类,就不是Class的实例;而当我们将Person.class加载到内存(或是说JVM)中进行解释运行时,此时这个Person类才称得上是Class的实例

2.换句话说,Class的实例就对应着一个运行时类

3.加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类

14.2.2 获取Class类的实例

//获取Class实例的四种方式(前三种需要掌握)
@Test
public void getClassExample() throws ClassNotFoundException {
    //1.方式一:调用运行时类的属性.class
    Class<Person> clazz1 = Person.class;
    System.out.println(clazz1);//class Reflection.Person
    //方式二:通过运行时类的对象调用getClass()方法
    Person p = new Person();
    Class clazz2 = p.getClass();
    System.out.println(clazz2);//class Reflection.Person
    //方式三:调用Class的静态方法:forName(String classPath)  使用最多
    Class clazz3 = Class.forName("Reflection.Person");
    System.out.println(clazz3);//class Reflection.Person

    System.out.println(clazz1 == clazz2);//true
    System.out.println(clazz1 == clazz3);//true
    System.out.println(clazz2 == clazz3);//true
    //方式四:使用类的加载器:ClassLoader(了解)
    ClassLoader classLoader = ReflectionTest02.class.getClassLoader();
    Class clazz4 = classLoader.loadClass("Reflection.Person");
    System.out.println(clazz4);//class Reflection.Person
    System.out.println(clazz1 == clazz4);//true
}

14.2.3 哪些类型可以作为Class对象

(1) class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类

(2) interface:接口

(3 []:数组

(4) enum:枚举

(5) annotation:注解@interface

(6) primitive type:基本数据类型

(7) void

@Test
public void Test(){
    Class c1 = Object.class;//对象
    Class c2 = Comparable.class;//接口
    Class c3 = String[].class;//一维数组
    Class c4 = int[][].class;//二维数组
    Class c5 = ElementType.class;//枚举类
    Class c6 = Override.class;//注解
    Class c7 = int.class;//基本数据类型
    Class c8 = void.class;//void
    Class c9 = Class.class;//Class本身

    int[] a = new int[10];
    int[] b = new int[100];
    Class c10 = a.getClass();
    Class c11 = b.getClass();
    //只要元素类型与维度一样,就是同一个Class
    System.out.println(c10 == c11);//true
}

14.3 类的加载与ClassLoader的理解

14.3.1 类的加载过程

加载:

  • 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与

链接:将Java类的二进制代码合并到JVM的运行状态之中的过程

  • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
  • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配
  • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程

初始化:

  • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的类构造器是构造类信息的,不是构造该类对象的构造器
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
  • 虚拟机会****

14.3.2 理解类加载器ClassLoader

1.类加载器的作用:
  • 类加载的作用将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口
  • 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象
2.获取三种类加载器
@Test
public void TestClassLoader(){
    //对于自定义类,使用系统类加载器进行加载
    ClassLoader classLoader = ReflectionTest03.class.getClassLoader();//获得系统类加载器
    System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

    //调用系统类加载器的getParent():获取扩展类加载器
    ClassLoader classLoader1 = classLoader.getParent();
    System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1f32e575,获取到的扩展类加载器

    //调用扩展类加载器的getParent():无法获取引导类加载器
    //引导类加载器主要负责java的核心类库,无法加载自定义类
    ClassLoader classLoader2 = classLoader1.getParent();
    System.out.println(classLoader2);//null,不能直接获取引导类加载器

    ClassLoader classLoader3 = String.class.getClassLoader();
    System.out.println(classLoader3);//null,说明String类是引导类加载的
}
3.使用ClassLoader加载配置文件
@Test
public void TestLoadProperty() throws IOException {
    Properties pros = new Properties();//用来读取配置文件

    //读取配置文件的方式一:此时的配置文件默认在当前项目(或Module)下
    //FileInputStream fileInputStream = new FileInputStream("jdbc.properties");
    //pros.load(fileInputStream);

    //读取配置文件的方式二:此时的配置文件默认在当前项目(或Module)的src下
    ClassLoader classLoader = ReflectionTest03.class.getClassLoader();
    InputStream inputStream = classLoader.getResourceAsStream("jdbc1.properties");
    pros.load(inputStream);

    String name = pros.getProperty("name");
    String password = pros.getProperty("password");
    System.out.println("name = " + name + ", password = " + password);//方式一:name = Tom, password = abc123            方式二:name = Mary, password = 123
}

14.4 创建运行时对象

14.4.1 通过反射创建运行时类对象

//通过反射,创建运行时类对象
/*
newInstance():调用此方法,创建对应的运行时类的对象
要想此方法正常的创建运行时类的对象,

要求:1.运行时类必须提供空参的构造器
2.空参的构造器的访问权限得够。通常,设置为public,如果想访问private构造器,需要调用setAccessible()方法设置可调用性
在javabean中要求提供一个public的空参构造器。

原因:1.便于通过反射,创建运行时类的对象
2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器
*/
Class clazz = Person.class;
Person p1 = (Person) clazz.newInstance();
System.out.println(p1);
/*
或者
Class<Person> clazz = Person.class;
*/

14.4.2 体会反射的动态性

//体会反射的动态性
//创建一个指定类的对象。classPath指定类的全类名
public Object getInstance(String classPath) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    Class clazz = Class.forName(classPath);
    return clazz.newInstance();
}

@Test
public void TestDynamic(){
    int num = new Random().nextInt(3);//0、1、2
    String classPath = "";
    switch (num){
        case 0:
            classPath = "java.util.Date";
            break;
        case 1:
            classPath = "java.lang.Object";
            break;
        case 2:
            classPath = "Reflection.Reflection03";
            break;
    }
    try {
        Object obj = getInstance(classPath);
        System.out.println(obj);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

14.5 获取运行时类的完整结构

14.5.1 提供一个结构丰富的自定义类Person

//自定义类
@MyAnnotation
public class Person1 extends Creature<String> implements Comparable<String>,MyInterface{

    private String name;
    int age;
    public int id;

    public Person1() {
    }

    @MyAnnotation(value = "constructor")
    private Person1(String name) {
        this.name = name;
    }

    Person1(String name,int age){
        this.name = name;
        this.age = age;
    }

    @MyAnnotation(value = "nation")
    private String show(String nation){
        System.out.println("我的国籍是" + nation);
        return nation;
    }

    public String display(String interest,int age) throws NullPointerException,ClassCastException{
        return interest + age;
    }

    @Override
    public void Info() {
        System.out.println("这是一个人");
    }

    @Override
    public int compareTo(String o) {
        return 0;
    }
    
    private static void showDesc(){
        System.out.println("我是Person1类的静态方法");
    }
    
    @Override
    public String toString() {
        return "Person1{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }
}
//自定义类的父类
public class Creature<T> implements Serializable {
    private char gender;
    public double weight;

    private void breath(){
        System.out.println("生物呼吸");
    }

    public void eat(){
        System.out.println("生物吃东西");
    }
}
//自定义类的注解
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
    String value() default "hello";
}
//自定义类实现的接口
public interface MyInterface {
    void Info();
}

14.5.2 获取运行时类的属性结构及其内部结构

//获取运行时类的属性结构
@Test
public void TestGetField(){
    Class<Person1> clazz = Person1.class;
    //获取属性结构
    //getFields():获取当前运行时类及其父类中声明为public访问权限的属性
    Field[] fields = clazz.getFields();
    for (Field field : fields) {
        System.out.println(field);
        //public int Reflection.Person1.id
        // public double Reflection.Creature.weight
    }
     System.out.println("*********************");
    //getDeclaredFields():获取当前运行时类中声明的所有属性(不包含父类中声明的属性)
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        System.out.println(declaredField);
        //private java.lang.String Reflection.Person1.name
        // int Reflection.Person1.age
        // public int Reflection.Person1.id
    }
    System.out.println("*********************");
    
    //获取属性的权限修饰符,数据类型,变量名
    for (Field declaredField : declaredFields) {
        //权限修饰符
        int modifier = declaredField.getModifiers();
        System.out.print(Modifier.toString(modifier) + "\t");
        //数据类型
        Class type = declaredField.getType();
        System.out.print(type + "\t");
        //变量名
        String name = declaredField.getName();
        System.out.println(name);
        //private    class java.lang.String    name
        //    int    age
        //public    int    id
    }
}

14.5.3 获取运行时类的方法结构及其内部结构

//获取运行时类的方法结构及其内部结构
@Test
public void TestGetMethod(){
    Class<Person1> clazz = Person1.class;
    //getMethods():获取当前运行时类及其所有父类中声明为public权限的方法
    Method[] methods = clazz.getMethods();
    for (Method method : methods) {
        System.out.println(method);
        //public int Reflection.Person1.compareTo(java.lang.String)
        //public int Reflection.Person1.compareTo(java.lang.Object)
        //public void Reflection.Person1.Info()
        //public java.lang.String Reflection.Person1.display(java.lang.String,int) throws java.lang.NullPointerException,java.lang.ClassCastException
        //public void Reflection.Creature.eat()
        //public final void java.lang.Object.wait() throws java.lang.InterruptedException
        //public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
        //public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
        //public boolean java.lang.Object.equals(java.lang.Object)
        //public java.lang.String java.lang.Object.toString()
        //public native int java.lang.Object.hashCode()
        //public final native java.lang.Class java.lang.Object.getClass()
        //public final native void java.lang.Object.notify()
        //public final native void java.lang.Object.notifyAll()
    }
    System.out.println("************************");
    //getDeclaredMethods():获取当前运行时类中声明的所有方法(不包含父类中的方法)
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
        System.out.println(declaredMethod);
        //public int Reflection.Person1.compareTo(java.lang.String)
        //public int Reflection.Person1.compareTo(java.lang.Object)
        //public void Reflection.Person1.Info()
        //public java.lang.String Reflection.Person1.display(java.lang.String,int) throws java.lang.NullPointerException,java.lang.ClassCastException
        //private java.lang.String Reflection.Person1.show(java.lang.String)
    }

    System.out.println("*************************");
    //获取方法的内部结构
    // @Xxx(注解)
    // 权限修饰符 返回值类型 方法名 (参数值类型1 形参名1) throws XxxException {}
    for (Method declaredMethod : declaredMethods) {
        //1.获取方法声明的注解
        Annotation[] annotations = declaredMethod.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);//@Reflection.MyAnnotation(value=nation)
            //能被获取到的注解其元注解Retention的值需要是RUNTIME
        }
        //2.获取方法的权限修饰符
        System.out.print(Modifier.toString(declaredMethod.getModifiers()) + "\t");
        //3.获取方法的返回值类型
        System.out.print(declaredMethod.getReturnType().getName() + "\t");
        //4.获取方法名
        System.out.print(declaredMethod.getName());
        //5.获取形参列表
        //方法一:
        System.out.print("(");
        Class[] parameterTypes = declaredMethod.getParameterTypes();//获取到形参列表的所有参数类型
        if(!(parameterTypes == null || parameterTypes.length == 0)){//判断是否为空
            for (int i = 0;i < parameterTypes.length;i++) {//遍历形参列表
                if (i == parameterTypes.length-1){//判断是否为形参列表最后一个参数,若是就不用加",",然后跳出遍历循环
                    System.out.print(parameterTypes[i].getName() + " " + "arg_" + i);
                    break;
                }
                System.out.print(parameterTypes[i].getName() + " " + "arg_" + i + ",");
            }
        }
        System.out.print(")");
        //方法二:
        System.out.print("(");
        Parameter[] parameters = declaredMethod.getParameters();
        if (!(parameters == null || parameters.length == 0)){
            for (int i = 0;i < parameters.length;i++) {
                if (i == parameters.length - 1){
                    System.out.print(parameters[i]);
                    break;
                }
                System.out.print(parameters[i] + ",");
            }
        }
        System.out.print(")");
        //6.获取抛出的异常
        Class[] exceptionTypes = declaredMethod.getExceptionTypes();
        if (!(exceptionTypes == null || exceptionTypes.length == 0)){
            System.out.print(" throws ");
            for (int i = 0; i < exceptionTypes.length; i++) {
                if (i == exceptionTypes.length - 1){
                    System.out.print(exceptionTypes[i].getName());
                    break;
                }
                System.out.print(exceptionTypes[i].getName() + ",");
            }
        }
        System.out.println();
        //public    int    compareTo(java.lang.String arg_0)
        //public volatile    int    compareTo(java.lang.Object arg_0)
        //public    void    Info()
        //public    java.lang.String    display(java.lang.String arg_0,int arg_1) throws java.lang.NullPointerException,java.lang.ClassCastException
        //@Reflection.MyAnnotation(value=nation)
        //private    java.lang.String    show(java.lang.String arg_0)
    }
}

14.5.4 获取运行时类的构造器结构

//获取运行时类的构造器结构
@Test
public void TestGetConstructor(){
    Class<Person1> clazz = Person1.class;
    //getConstructor():获取当前运行时类中声明为public的构造器(不包含父类构造器)
    Constructor[] constructors = clazz.getConstructors();
    for (Constructor constructor : constructors) {
        System.out.println(constructor);//public Reflection.Person1()
    }
    //getDeclaredConstructors():获取当前运行时类中声明的所有构造器
    Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
    for (Constructor<?> declaredConstructor : declaredConstructors) {
        System.out.println(declaredConstructor);
        //Reflection.Person1(java.lang.String,int)
        //private Reflection.Person1(java.lang.String)
        //public Reflection.Person1()
    }
    //获取运行时类的构造器内部结构跟方法一样,使用获取到的构造器再调用相应的获取修饰符、构造器名字、参数类型、参数名的方法即可
}

14.5.5 获取运行时类的父类及父类泛型

//获取运行时类的父类及父类泛型
@Test
public void TestGetParent(){
    Class<Person1> clazz = Person1.class;
    //getSuperClass():获取运行时类的父类
    Class superclass = clazz.getSuperclass();
    System.out.println(superclass);//class Reflection.Creature
    //getGenericSuperClass():获取运行时类的带泛型的父类
    Type genericSuperclass = clazz.getGenericSuperclass();
    System.out.println(genericSuperclass);//Reflection.Creature<java.lang.String>
    //getActualTypeArguments():获取运行时类的带泛型的父类的泛型
    ParameterizedType paramType = (ParameterizedType) genericSuperclass;//将泛型父类对象转换成带泛型参数的父类对象
    Type[] actualTypeArguments = paramType.getActualTypeArguments();//获取具体泛型参数类型
    for (Type actualTypeArgument : actualTypeArguments) {
        System.out.println(actualTypeArgument.getTypeName());//java.lang.String
    }
}

14.5.6 获取运行时类的接口、所在包、注解等

//获取运行时类的接口、所在包、注解等
@Test
public void TestGetOther(){
    Class<Person1> clazz = Person1.class;
    //getInterfaces():获取运行时类实现的接口
    Class[] interfaces = clazz.getInterfaces();
    for (Class anInterface : interfaces) {
        System.out.println(anInterface);//interface java.lang.Comparable   interface Reflection.MyInterface
    }
    //获取运行时类父类实现的接口
    Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
    for (Class aClass : interfaces1) {
        System.out.println(aClass);//interface java.io.Serializable
    }
    //获取运行时类所在的包
    Package aPackage = clazz.getPackage();
    System.out.println(aPackage);//package Reflection
    //获取运行时类的注解
    Annotation[] annotations = clazz.getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println(annotation);//@Reflection.MyAnnotation(value=hello)
    }
}

14.6 调用运行时类的指定结构

14.6.1 调用运行时类的指定属性

//调用运行时类的指定属性 -- 需要掌握
@Test
public void TestTransferField() throws InstantiationException, IllegalAccessException, NoSuchFieldException {
    Class<Person1> clazz = Person1.class;
    //创建运行时类的对象
    Person1 person = clazz.newInstance();
    //getField()获取指定的属性:要求运行时类中属性声明为public
    Field id = clazz.getField("id");
    //设置当前属性的值   set():参数1指明哪个对象的属性   参数2将此属性值设置为多少
    id.set(person,1001);
    //获取当前属性的值   get():参数1获取哪个对象的当前属性值
    int pId = (int) id.get(person);
    System.out.println(pId);//1001

    //getDeclaredField(String fieldName)获取运行时类中指定变量名的属性
    Field name = clazz.getDeclaredField("name");
    name.setAccessible(true);//保证当前属性是可访问的
    //获取、设置当前对象的属性值
    name.set(person,"Tom");
    System.out.println(name.get(person));//Tom
}

14.6.2 调用运行时类的指定方法

//调用运行时类的指定方法
@Test
public void TestTransferMethod() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    Class<Person1> clazz = Person1.class;
    //创建运行时类的对象
    Person1 person = clazz.newInstance();
    //获取指定的某个方法(调用非静态方法)
    //getDeclaredMethod():参数1∶指明获取的方法的名称﹑参数2:指明获取的方法的形参列表
    Method show = clazz.getDeclaredMethod("show", String.class);
    show.setAccessible(true);//保证当前方法可访问
    //invoke():参数1:方法的调用者,参数2:给方法形参赋值的实参,其返回值为对应类中调用该方法的返回值
    Object chn = show.invoke(person, "CHN");//类似于String chn = person.show("CHN");
    System.out.println(chn);//CHN

    //调用静态方法
    Method showDesc = clazz.getDeclaredMethod("showDesc");
    showDesc.setAccessible(true);
    Object invoke = showDesc.invoke(Person.class);//类似于Person.showDesc()
    System.out.println(invoke);//null,方法没有返回值则invoke返回null
}

14.6.3 调用运行时类的指定构造器

//调用运行时类的指定构造器
@Test
public void TestTransferConstructor() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    //一般情况都用newInstance()方法调用空参构造器创建对象

    Class<Person1> clazz = Person1.class;
    //调用指定构造器
    //getDeclaredConstructor():参数:指明要调用的构造器的参数列表
    Constructor<Person1> constructor = clazz.getDeclaredConstructor(String.class);
    constructor.setAccessible(true);//保证当前构造器可访问
    //调用此构造器创建运行时类对象
    Person1 person = constructor.newInstance("Tom");
    System.out.println(person);//Person1{name='Tom', age=0, id=0}
}

14.7 反射的应用:动态代理

代理设计模式的原理:

使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上

之前5.6.2接口中提到的代理机制的操作,属于静态代理,特征是代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。同时,每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。最好可以通过一个代理类完成全部的代理功能

14.7.1 动态代理

动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象

动态代理使用场合:

  • 调试
  • 远程方法调用

动态代理相比于静态代理的优点:
抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法

14.7.2 静态代理实例

//静态代理测试
//特点:代理类和被代理类在编译期间就确定下来了
public class StaticProxyTest {
    public static void main(String[] args) {
        //创建被代理类对象
        ClothFactory nike = new NikeClothFactory();
        //用被代理类对象创建代理类对象
        ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nike);
        //调用代理类的方法
        proxyClothFactory.produceCloth();
    }
}

interface ClothFactory{
    void produceCloth();
}

//代理类
class ProxyClothFactory implements ClothFactory{
    private ClothFactory factory;

    public ProxyClothFactory(ClothFactory factory){
        this.factory = factory;
    }
    @Override
    public void produceCloth() {
        System.out.println("代理工厂做准备工作");
        factory.produceCloth();
        System.out.println("代理工厂做后续收尾工作");
    }
}
//被代理类
class NikeClothFactory implements ClothFactory{
    @Override
    public void produceCloth() {
        System.out.println("Nike工厂生产运动服");
    }
}

14.7.3 动态代理实例

//动态代理测试
//要想实现动态代理,需要解决的问题?
//问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象(动态代理的核心)
//问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a
public class DynamicProxyTest {
    public static void main(String[] args) {
        //创建被代理类对象
        SuperMan superMan = new SuperMan();
        //创建代理类对象
        //因为代理类和被代理类都会实现相同的接口,所以可以将代理类转换成接口对象
        Human proxyInstance = (Human) ProxyFactor.getProxyInstance(superMan);//此时的proxyInstance就是代理类的对象
        //当通过代理类对象调用方法时,会自动调用被代理类中同名的方法
        String belief = proxyInstance.getBelief();
        System.out.println(belief);
        proxyInstance.eat("四川麻辣烫");
        System.out.println("***********************");
        NikeClothFactory nikeClothFactory = new NikeClothFactory();
        ClothFactory proxyClothFactory = (ClothFactory) ProxyFactor.getProxyInstance(nikeClothFactory);
        proxyClothFactory.produceCloth();
    }
}

interface Human{
    String getBelief();
    void eat(String food);
}
//被代理类
class SuperMan implements Human{
    @Override
    public String getBelief() {
        return "I believe I can fly!";
    }

    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃" + food);
    }
}
//下方两个类的个人理解:代理类创建实例的步骤和调用代理类中与被代理类中的同名方法的步骤被分成两个类单独处理,由于被代理类的不确定性,则创建出的代理类的对象也具有不确定性,所以就体现出了动态代理的动态性
//根据被代理类创建代理类的工厂
class ProxyFactor{
    //调用此方法,返回一个代理类的对象
    public static Object getProxyInstance(Object obj){//obj:被代理类对象
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj);
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler);
        //newProxyInstance()中的参数:
        //ClassLoader loader:类加载器定义代理类
        //类<?>[] interfaces:代理类需要实现的接口
        //InvocationHandler h:调用被代理类的方法的具体处理类的对象
        //newProxyInstance()根据传入的被代理类的构造器、代理类需要实现哪些接口、代理类实例调用被代理类方法的具体处理类的对象来创建并返回一个代理类对象
    }
}
//代理类实例调用代理类中与被代理类中被调用的同名方法的具体类
class MyInvocationHandler implements InvocationHandler{
    private Object obj;//需要使用被代理类的对象进行赋值
    public void bind(Object obj){
        this.obj = obj;
    }
    //当我们通过代理类对象,调用方法a时,就会自动调用如下的方法:invoke()
    //将被代理类要执行的方法a的功能就声明在invoke()中
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//相当于是调用代理类中的method方法,然后再在里面调用被代理类的同名方法
        //proxy:调用该方法的代理实例
        //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
        //args:被调用方法的参数列表
        //obj:被代理类对象
        Object returnValue = method.invoke(obj, args);//类似obj.method(args)
        //上述方法的返回值就作为当前类中的invoke()的返回值
        return returnValue;
    }
}

14.7.4 动态代理与AOP

评论