30140 字
151 分钟
Java学习笔记
Java学习笔记,包括cmd命令行执行Java、注释方式、输出方法、public修饰符等基础知识讲解。
2024-02-28
0 次
0 人

Java 学习笔记一#

1.cmd 下执行 Java#

  1. 编译 java 文件

    先用“javac 文件名.java”命令,生成一个或多个后缀为 class、==文件名与类名相同==的文件(字节码文件)。

    javac 命令需要带“java”后缀。

  2. 执行 class 文件

    使用“java main()所在的类名”。

    java 命令无需带“.class”后缀。

执行过程

以上操作不能执行带 package 的 java 文件,若想执行带 package 的文件,需要使用额外的命令行参数。

dos执行java

2.注释#

//:单行注释。

/**/:多行注释。

编译后生成的 class 文件里没有注释里的内容,即写再多的注释,class 文件的大小也不会变。

/** *...*/:文档注释,用javadoc -encoding UTF-8 -d mydir -version -author HelloWorld.java命令后,会生成一个类似 API 文档的网页,注释里的内容会成为网页的一部分。

javadoc 和 java 文件间的内容都是命令行参数。

文档注释文档注释产生的网页

注意文档注释写的位置,有些注释在类外,有些注释在类里面。

3.输出#

System.out.println():输出数据之后,会换行。

当括号里不填内容时,相当于换行操作,等价于System.out.print("\n")

System.out.print():输出数据后,不换行。

4.pubilc 修饰符#

一个 Java 文件中可定义多个类,每个类都会生成一个 class 文件(字节码文件),且 class 文件名与类名相同。

public 修饰的类在每个 Java 文件中最多一个,且类名必须与 Java 文件名相同。

5.包(package)#

在 IDEA 中,一个 Project 可以有多个 Module,一个Module 下可以有多个 package,一个 package 下可以有多个类。无论是建立 New Project,还是 Empty Project 都会自带一个 Module。可以在 Project Structure 里删除这个 Module。

不同功能的类需要被放到不同的文件夹中。package 的命名采用域名倒置的规则,比如 www.lc.com下的包可以写成 com.lc.包名

6.标识符#

6.1 命名规则#

标识符可以包含字母,数字,_,$,但是不能以数字开头,不能包含空格;

6.2 命名习惯#

6.2.1 包名#

多单词组成时所有字母==都小写==。

例如:java.lang、com.atguigu.bean 等。

6.2.2 类名、接口名#

多单词组成时,所有单词的==首字母大写==。

例如:HelloWorld、String、System 等。

6.2.3 变量名、方法名#

多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写

例如:age、name、bookName、main、binarySearch、getName 等。

在同一作用域下,不能声明 2 个同名的变量。

6.2.4 常量名#

所有字母都大写。多单词时每个单词用下划线连接。

例如:MAX_VALUE、PI、DEFAULT_CAPACITY 等。

7.基本数据类型#

7.1 整型#

Java 各整数类型有==固定的==表数范围和字段长度,不受具体操作系统的影响,以保证 Java 程序的可移植性。

类型占用存储空间表示范围
byte1 字节-128 ~ 127
short2 字节-2^15^ ~ 2^15^-1
int4 字节-2^31^ ~ 2^31^-1(约21.4亿)
long8 字节-2^63^ ~ 2^63^-1

定义 long 类型的变量,赋值时需要以”l”或”L”作为后缀。

开发过程中,整型变量通常声明为 int 型。

整型常量默认为 int。

7.2 浮点型#

浮点型有==固定的==表数范围和字段长度,不受具体操作系统的影响。

类型占用存储空间
float4 字节
double8 字节

定义 float 类型的变量,赋值时需要以”f”或”F”作为后缀。

float、double 的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数字计算或保留指定位数的精度,需要使用 BigDecimal 类

浮点型常量默认为 double。

7.3 字符型#

char 占 2 个字节。值用单引号括起来,例子:char a = 'a';char b = '中';

char 类型用的是 Unicode 字符集,每个字符对应一个 Unicode 数值(没有涉及具体的编码方式,UTF-8 是一种可变长度编码方式,可以根据字符的不同使用1至4个字节进行编码。),把这个数值强转为 char 类型即可显示这个字符。

char 类型能被赋值 int 类型常量

final int b1= 23320; //少了final,就会过不了编译
char a = b1; //或直接是char a = 23320;
System.out.println(a); //能打印字符

7.4 布尔型#

boolean 类型数据只有两个值:true、false,无其它

不可以使用 0 或非 0 的整数替代 false 和 true,这点和 C 语言不同,布尔型不能和其他基本数据类型做运算。

8. 类型转换#

8.1 自动类型转换#

byte ,short,char ==> int ==> long ==> float ==> double

表示范围小的可以转变为表示范围大的,不是看占用存储空间大小。float 能表示的数比 long 还大。

注:只要有 char、byte 、short 参与的运算的结果都是 int。

byte a = 10;
byte b = a + 12;
//会报错,算式里的整数常量 12 默认是 int ,需要用 int 变量接收
//浮点数同理,而浮点数常量默认为 double。
System.out.println('A' + 1); //char 与 int 运算,结果为int,即 66。
System.out.println('A'== 65); //“==”也是运算,涉及自动类型转换,结果为 true。
int a = 'c'; //这里也涉及自动转型
System.out.println(a); //99

8.2 强制类型转换#

和 C 语言类似,但 String 类型不能通过强制类型转换为基本数据类型,因为 String 不是基本数据类型。

强转改变了字节数,可能导致数值变化。

int b = 10;
byte a = (byte)b;

9.引用数据类型#

9.1 字符串#

String 和基本数据类型(包括布尔型)可以用“+”拼接。

String a = "abc";
System.out.println(a+1); //结果是abc1,类型为字符串
System.out.println(a+true); //结果是abctrue

10.进制#

10.1 二进制#

0b0B开头。

int a = 0b110;//或者0B110
System.out.println(a); //输出为:6,以10进制输出

10.2 八进制(少用)#

0开头。

int b = 012;
System.out.println(b); //输出为10,以10进制输出

10.3 十六进制#

0x0X开头,a-f不区分大小写。

int a = 0x110;//或者0X110
System.out.println(a); //输出为:272,以10进制输出

10.4 补码(符号位不参与运算)#

整型是以==二进制补码==形式存储的,并且最高位是符号位。

  • 正数:最高位是 0
  • 负数:最高位是 1

正数的补码、反码、原码一样,称为三码合一

负数的补码、反码、原码不一样:

  • 负数的原码:把十进制转为二进制,然后最高位设置为 1。

  • 负数的反码:在原码的基础上,==最高位不变==,其余位取反(0 变 1,1 变0)。

  • 负数的补码:反码加 1。

求补码最快的方法是:除符号位、==最后个 1 及以后的位==不变,其余位全取反。

补码的补码为原码。

补码转原码:最高位不变,其余位取反再加 1,因此也可以使用最快方法

11.运算#

11.1 运算符的细微差异#

java 中的 % 是取余,商向 0 靠近,被除数 = 除数 * 商 + 余数。

System.out.print(-5%2); //本身商为 -2.5,但是向0取整则为 -2,余数为 -1
System.out.print(-5/2); //本身商为 -2.5,但是向0取整则为 -2,

python中 % 为取模,商向负无穷靠近。

print(5//2) #地板除,得 2.
print(5/2) #得2.5
print(-5%2) #本身商为 -2.5,但是向负无穷取整得 -3,取模为 1,-5 = 2*(-3) + 1

a+=2;a= a+2;的细微差别,a+=2; int b =1000; a+=b;中 a 的类型不会改变,比如 a 的类型为 byte,则运算完也为 byte,只是把高位抛弃,只看地位对应哪个值, -=、*=、 /=、%= ++具有相同的性质。

short s1 = 10;
s1 += 2; //因为在得到 int 类型的结果后,JVM 自动会进行强制类型转换,将 int 类型强转成 short
s1++;

11.2 比较运算符#

  1. 比较运算符的结果都是 boolean 型,也就是要么是 true,要么是 false。
  2. > < >= <= :只适用于基本数据类型(除 boolean 类型之外)。
  3. == != :适用于基本数据类型和引用数据类型。

11.3 逻辑运算符#

& &&:表示“且”。

| || :表示“或”。

a&ba&&b的区别在于 a 为 false 时,&仍会判断b,从而执行b中的表达式,比如age++;而&&短路,不再判断 b。|||同理,推荐使用&&||

^:表示“异或”。

!:表示“非”。

11.4 位运算符(运算用补码,看结果用原码)#

位运算涉及到了自动类型转换(byte、short 移位后会变为int,且机制复杂,可以不用掌握)。

a<<n:左移 n 位,低位补0。在一定范围内,相当于乘 2^n^(正负数都是)。

有可能会移成负数,即把数值的 1 移到符号位。

当左移的位数 n 超过该数据类型的总位数时,相当于左移(n % 总位数)位,比如 int 是模32,long是模 64(不考虑 byte、short 的情况)。

a>>n:右移 n 位,高位补==与符号位相同的值==。在一定范围内,相当于除 2^n^,然后向下取整(正负数都是)。

a>>>n::右移 n 位,高位==补 0==。(正数、负数都适用)。

负数会变成正数。

a&b(与)、a|b(或)、~a(非)、a^b(异或)

11.5 条件运算符#

c = 条件表达式 ? A : B;表达式为true执行 c = A,表达式为false执行 c = B

也可以结合输出,syetem.out.println(条件表达式 ? "我是A" : "我是B");

image-20220312002841945

返回值的类型为两个返回值中精度更高的那个类型。

System.out.println(true ? 1 : 2.0); //自动类型提升,返回的是 1.0

12.switch-case#

case 子句中的值必须是==常量==,不能是变量名或不确定的表达式值或范围。

switch() 中的表达式结果的类型只能是 byte,short,int,char,String 和枚举,不能是 boolean 类型。

switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
// ...
[default:
语句块n+1;
break;
]
}

13.break 和 continue#

break 语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块,continue 类似。

label1: for(...){ //for1
label2:for(...){ //for2
label3:for(...){ //for3
break label2; //跳出label2后的这个循环,即for2,再执行for1所在的循环。
}
}
}

continue 结束此次循环,然后再次进入所属循环语句的下轮循环。 若上面 break 换成 continue,则是跳出 for2,再进入 for2。

标签不能用在非循环语句的前面。

14.Scanner#

步骤:

  1. 导包:import java.util.Scanner;

  2. 创建 Scanner 对象:Scanner scan = new Scanner(System.in);

  3. next() / nextXxx(),来获取指定类型的变量,如nextInt() 获取一个 Int 型整数,nextDouble() 获取一个 Double 型浮点数。

    • 我们把 Scanner 看为 2 个过程,扫描和键入。扫描是去输入缓冲区==取数据==;键入是从键盘输入数据,以“换行符”作为结束的标志,即按下“Enter”键后才会结束。

    • 先扫描,再键入,若缓冲区有数据则无需键入。

    • next()nextLine()都是读取字符串的方法。

    • next()nextLine()的键入过程相同,扫描过程不同。

      • nextLine()会读入“空格”,即将“空格”看为字符串的一部分,只把“换行符”作为扫描结束的标志,并且把光标移到“换行符”后。

      • next()会忽略掉==有效字符之前==遇到的“空格”以及“换行符”,把有效字符后的“空格”或“换行符”作为扫描结束的标志,并且光标停在结束符之前

        nextInt()、nextDouble()等和next()扫描过程相同,只有nextLine()很特殊。

    • 这就会出现一个问题,先使用nextInt(),光标停在结束符之前,nextLine()可能会读到空字符串或者带空格的脏数据。解决办法:使用nextInt()后,先调用nextLine()把脏数据读走,将光标移到“换行符”后,再用nextLine()读取需要的数据,也就是使用 2 次nextLine()

    • Scanner scanner = new Scanner(System.in);
      String a = scanner.next(); //输入:123 qweqw
      String b = scanner.nextLine();
      //先扫描缓冲区,发现还有" qweqw",则没有键入过程。
      System.out.println(a); //读走"123",剩下" qweqw"以及换行符
      System.out.println(b); //“换行符”是扫描结束的标志,则" qweqw"
      scanner.close();
  4. 释放资源:scan.close();

15.Math.random()#

Math.random()会返回范围在 [0,1) 的 double 浮点数。

获取 [a,b] 范围内的随机整数(int)(Math.random() * (b - a + 1))+a,取整使得范围从 [a,b+1) 变成[a,b]。

16.数组#

数组本身是==引用数据类型==,而数组中的元素可以是==任何数据类型==(基本数据类型或引用数据类型)。

16.1 一维数组#

16.1.1 声明数组#

类型[] 数组名;,例如int[] arr;

Java 中声明数组时,不能指定长度。

未初始化的数组不能直接使用。

16.1.2 初始化#

  • 静态初始化

    初始化的同时赋初值。

    • 写法 1:int[] arr = new int[]{1,2,3,4,5};
    • 写法 2:int[] arr = {1,2,3,4,5};,这种不能写成int[] arr; arr = {1,2,3,4,5};,因为编译器识别出不来。
  • 动态初始化

    • int[] arr = new int[5];,[] 中指定数组长度,但不赋初值。

    • int n = nums.length; //创建一个和nums相同长度的数组 int[] ans = new int[n]; ,在 Java 中,可以将数组长度设置一个变量,只要在初始化之前给这个变量赋值。

      • 指定长度后,无法修改长度。

      • 数组的 length 属性可以得到数组的容量。

      • 当使用动态初始化创建数组时,元素值是默认值。

      默认初始化

16.1.3 一维数组内存存储#

public static void main(String[] args) {
int[] arr = new int[3];
System.out.println(arr); //打印的是哈希值,不是内存地址
}
一维数组内存

只有在 new 数组对象时,才会在堆中分配。声明数组只会在栈中分配。

16.2 二维数组#

16.2.1 声明二维数组#

可以理解为一维数组的元素也是一维数组。

二维数组

元素的数据类型[][] 二维数组的名称;,例如int[][] grades;

16.2.2 初始化#

  • 静态初始化

    • int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}};
    • int[][] arr = {{1,2,3},{4,5,6},{7,8,9,10}};
  • 动态初始化

    • 指定内层一维数组长度,int[][] arr = new int[3][2];
    • 不指定内层一维数组长度,int[][] arr = new int[3][]; arr[i] = new int[长度];,在使用时,才具体指定 arr[i] 的长度。

    注:

    1. arr.length 得到的是new int[3][2];中的 3,即一个二维数组能存储 3 个一维数组;arr[0].length 得到的是第一个一维数组的长度,即 2。

    2. System.out.println(arr[i]);打印的是arr[i]的哈希值或者null

    3. 若没指定内层一维数组的长度(arr[i] = new int[长度];),使用arr[i].lengthSystem.out.println(arr[i][j]);都会报错。

二维数组内存#

二维数组内存

16.3 Arrays 工具类#

java.util.Arrays 类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。

  • 打印数组

Arrays.toString(arr):打印数组所有元素,形式为:[元素1,元素2,元素3...]

当 arr 是基本数据类型数组时,直接打印数值。

当 arr 是引用数据类型数组时,每个元素调用该引用类型的 toString()。没有重写 toString(),则打印哈希值。

二维数组的内层元素也是引用类型,调用其的 toString() 打印哈希值,若想把内层数组的值一起打印出来,需要用 Arrays.deepToString()

int[] arr = {1,2,3,4,5};
System.out.println(Arrays.toString(arr));
//[1, 2, 3, 4, 5]
Person person1 = new Person();
Person person2 = new Person();
Person[] arr = new Person[]{person1,person2};
System.out.println(Arrays.toString(arr));
//[Person{name='lc', age=10}, Person{name='lc', age=10}]
int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}};
System.out.println(Arrays.deepToString(arr)); //[[3, 8, 2], [2, 7], [9, 0, 1, 6]]
System.out.println(Arrays.toString(arr)); //[[I@214c265e, [I@448139f0, [I@7cca494b]
  • 排序

    • static void sort(int[] a) :将 a 数组按照值从小到大进行排序。

      int[] arr = {1,2,3,43,5};
      Arrays.sort(arr);
      System.out.println(Arrays.toString(arr));//[1,2,3,5,43]

      除 boolean 外的基本数据类型,都能排序。

    • static void sort(Object[] a)根据元素的自然顺序,对对象数组升序进行排序。自然顺序通过重写 Comparable 接口的 compareTo(obj o2) 实现。

    • static <T> void sort(T[] a, Comparator<? super T> c) :根据比较器产生的顺序,对指定对象数组进行升序排序。比较器通过重写 Comparator 接口的 compare(obj o1,obj o2) 实现。

      可以通过修改 compare()compareTo() 的实现 ,达到降序排列的目的,具体内容见 Java 学习笔记三的 9.1 小节。

  • 比较两个数组的内容是否相等

    • static boolean equals(int[] a, int[] a2) :比较两个数组的长度相同下标的元素的数值是否完全相同。

      int[] a = new int[]{1,2,3,4};
      int[] b = new int[]{1,2,3,4};
      System.out.println(Arrays.equals(a,b));//true
    • static boolean equals(Object[] a,Object[] a2):比较两个数组的长度相同下标对象的内容是否完全相同。

      Person person = new Person();
      Person person1 = new Person();
      Person person2 = new Person();
      Person[] p = new Person[]{person,person1};
      Person[] p1 = new Person[]{person,person2};
      System.out.println(Arrays.equals(p,p1)); //根据Person的equals判断

      通过该类的 equals()方法比较对象的内容,若没重写 equals()方法(相当于“==”),则比较的是“是否为同一个对象”。

Java学习笔记二#

1.面向对象(Object Oriented)#

类(Class)和 对象(Object)是面向对象的核心概念。

  • :具有相同特征的事物的抽象描述。

  • 对象实际存在的的个体 ,是具体的 ,因而也称为实例(instance)。

一个类由属性(也叫成员变量)和成员方法组成。

  • 属性跟随对象放在堆中,指向此对象的引用(如 p1)放在栈中。
  • 成员变量可以分为:
    • 类变量(以 static 修饰)
    • 实例变量(不以 static 修饰)
  • 与局部变量相比,成员变量前面能添加权限修饰符,且成员变量自带默认值。
  • 引用 p1 只存储了此对象的地址值。
  • 局部变量在使用之前必须赋初值,没用默认值
  • 在一个类中,一个成员方法可以使用这个类中的其余方法(包括自身,即形成递归)以及成员变量,但不能在方法中再定义方法
  • 在成员方法中,调用其他的成员方法,可以直接通过函数名(参数)调用(省略了“this.”)。
对象1对象2

1.1 方法重载(Overload,两同一不同)#

在同一个中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。

与权限修饰符、返回值类型、形参名无关,比如public void add(int m,int n)public void add(int n,int m)就不算重载。

class Human{
int a ;
public void Method(){
}
protected int Method(int a){ //两个Method构成了重载
return 12;
}
}

1.2 可变形参#

格式:方法名(参数的类型名…参数名),例如public static void test(int a ,String...books){}

特点:

  1. 方法的参数个数可以是 0 个或多个。

  2. 可变形参的方法与同名的方法之间,彼此构成重载。例如,public void test(int a){}public int void test(int...a){}能同时存在。当调用同名函数时,优先级:形参列表确定的函数 > 可变形参的函数

  3. 可变形参形参是数组是等价的,二者不能同时声明,否则报错。

public static void test(int a,String[] books){}与例子写法是等价的。

  1. 可变形参需要放在形参列表的==最后==。

  2. 在一个方法中,最多声明一个可变形参。

1.3 参数传递机制-值传递#

形参是基本数据类型:将实参的“数据值”传递给形参。

形参是引用数据类型:将实参的“地址值”传递给形参。

前提是实参和形参类型一致。

1.4 import#

导入==其他包==下的类。格式:import 包名.类名;

  • import 包名.*:导入包下面的所有类。
  • 类所在包的其他类 和 java.lang 无需导入,可以直接使用。
  • 导包导入的是当前文件夹里的类,不包括其子文件夹里的类。

比如使用import com.*; 是指导入 com 目录下的所有类,这里只有 Test 类,并不会导入在 com 的下一级 lc 目录中的 Phone 类。

导包导包2
  • 如果在代码中使用不同包下的同名的类,那么就需要使用全类名(包名+类名)的方式指明调用的是哪个类。

    如:java.sql.Date date = new java.sql.Date(time);

1.5 封装性#

1.5.1 权限修饰符#

Java 规定了 4 种权限修饰符,借助这些权限修饰符修饰类及类的内部成员。当这些成员被调用时,体现了可见性的大小。

权限修饰符本类内部本包内其他包的子类其他包的非子类
private
缺省
protected
public

注意:

  • 外部类只能用 public 和缺省修饰。

  • 对 protected 的理解:

    1. 在其他包的子类 A 中,可以==直接使用==或通过==子类 A 的对象==间接使用父类中 protected 修饰的属性或方法(可以只记这条,其他都无法访问)。
    2. 在其他包的子类中 ,==new 出来的父类对象==,无法使用父类中 protected 修饰的属性或方法。
    3. 其他包的类,通过调用子类对象,无法使用父类中 protected 修饰的属性或方法。
    4. 在其他包的子类 A 中,通过==子类 B 的对象==,无法使用父类的 protected 修饰的属性和方法。

    这里的“使用”是指:直接使用属性或通过“对象.属性”访问,而非通过如 getter 的方法访问。

    protected1

    <img src="https://oss.tortb.com/JavaStudyNoteImg/protected.png" alt="protected" style="zoom:80%;" />

1.6 构造器(Constructor)#

搭配 new 关键字创建对象。

public class Student {
private String name;
private int age;
// 无参构造,当程序员没显式给出构造器时,系统会默认调用无参构造且构造器的修饰符与类相同;当程序员给出有参构造时,则默认无参构造会失效。
public Student() {}
// 有参构造,有参构造可以给私有变量赋初值。
public Student(String n,int a) {
name = n;
age = a;
}
//构造器名与类名相同,构造器没有返回值。构造器可以重载。
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
//调用无参构造创建学生对象
Student s1 = new Student();
//调用有参构造创建学生对象
Student s2 = new Student("张三",23);

1.7 Java Bean#

一种特殊的类,具有以下属性:

1、具有一个 public 修饰的无参构造函数。

2、所有属性由 private 修饰。

3、通过 public 修饰的 get 方法和 set 方法获得或修改属性。

4、可序列化。

实现 Serializable 接口,用于实现bean的持久性。

1.8 this 关键字#

当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加 this 来表明该变量是类的成员变量。即:我们可以用 this 来区分成员变量和局部变量。

this 代表当前对象或即将创建的对象,Base sub = new Sub();。这里的对象是 Sub 对象,通过看 new 后面的类名判断是何种对象。

this 可以作为一个类中构造器相互调用的特殊格式。

  • this():调用本类的无参构造器。
  • this(实参列表):调用本类的有参构造器。

this()this(实参列表)只能声明在构造器首行,且不能出现递归调用。

public class Student{
private String name;
private int age;
//无参构造
public Student(){}
//有参构造
public Student(String name){
this.name = name;
}
public Student(String name,int age){
this(name); //此行代表先调用 public Student(String name) 这个构造器,减少了代码量。
this.age = age; //这里的 this 代表即将创建的对象。
}
public void setName(String name){
this.name = name; //this代表当前对象,“this.”修饰的变量代表成员变量。
}
}

1.9 继承性#

继承的出现让类与类之间产生了 is-a 的关系,比如: A student is a person。

关键字:extends,Java 中的继承是一种扩展,写法:public class B extends A{}

子类会继承父类中实现了的接口方法。

子类不能直接访问父类中私有的(private)的成员变量,需通过 get/set 方法。

一个父类可以同时拥有多个子类,但一个子类只有一个父类。

类加载的时候,先加载父类,再加载子类。所以先加载父类的静态代码块

1.10 重写(overwrite、override)#

子类重写从父类继承来的方法,以便适应新需求。

@Override:写在方法上面,用来检测是否满足重写的要求。这个注解就算不写,只要满足要求,也算重写。

override

重写的要求:

  • 方法名和参数列表不能变。

  • 子类重写的方法的权限修饰符不能小于父类被重写方法的权限修饰符。

    因为多态是以父类的引用类型调用子类的方法,必须保证父类方法能运行的地方,子类重写后的方法也能执行,所以权限不能变小。

    ① 父类私有方法不能重写。 ② 跨包父类中缺省的方法不能重写。

  • 若返回值类型是基本数据类型和 void,则不能改变。

  • 若返回值类型是引用数据类型,则返回值类型不能大于父类中被重写的方法的返回值类型。(例如:Student < Person)。

    也就是,返回值类型不变或是返回值类型的子类。原因是返回值可能赋给父类引用类型,这里也涉及了多态。不能把父类的对象赋值给子类引用类型的变量。

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

    还是和多态有关,父类的引用类型调用子类的方法,若子类抛出了更大的异常,针对父类写的 try-catch 语句就捕获不到子类的异常。

  • static 修饰的方法不能被重写。

1.11 super 关键字#

1.11.1 属性就近原则#

super使得在子类中可以使用父类的属性、方法、构造器。

一般可以省略,但是当出现==重写了父类的方法==或==子类、父类有重名属性==的情况,调用父类的方法或属性需要加上“super.”。

方法前面没有 super.this.(估计相当于省略了this.):先从子类找,如果没有,再从父类找,再没有,继续往上追溯。

方法前面有 this.:先从子类找,如果没有,再从直接父类找,再没有,继续往上追溯。

方法前面有 super.:==忽略==子类的方法,直接从父类找,如果没有,继续往上追溯。

总结:有super.则直接从父类找,没有super.则从子类开始找。

当方法需使用重名的属性时,采用就近的原则。

  • 子类中定义过的方法(即子类中重写过的方法子类中新定义的方法,不包括从父类继承但没重写的方法)优先找子类的属性。

  • 在子类中,重写过的方法前加上 super.后,使用的是父类的方法,所以就近找父类的属性。

  • 没有重写过的方法(看成父类的方法),在子类中调用,就近访问的是父类的属性。

    父类 同名属性的理解 重名的理解

父类中的 setInfo()没有在子类中重写,若在代码中调用,则修改的是父类的 Info 属性。

1.11.2 子类调用父类的构造器#

super(形参列表),必须声明在构造器的首行。

对于每个构造器,“this(空/形参列表)” 和 “super(空/形参列表)”只能二选一。没有显式给出”this(空/形参列表)”的构造器,默认调用”super()”,即调用父类中空参的构造器。那么父类空参构造器中已初始化的属性无需在子类构造器中反复初始化,除非初始值不同。

super

当我们用子类构造器创建对象时,子类构造器会直接或间接调用到其父类的构造器,而其父类的构造器会直接或间接地调用它自己父类的构造器,直到调用了 Object 类中的构造器,所以内存中就有父类声明的属性及方法。

1.12 多态性#

多态的使用前提:① 类的继承关系 ② 方法的重写

Java 中多态性的体现:**父类的引用可以指向子类的对象。**比如:Person p = new Student();

一种引用可以指向多种对象。

常用多态的地方:

  1. 方法的形参

  2. 方法的返回值

父类引用作为方法的形参,是多态使用最多的场合。即使增加了新的子类,原方法也无需改变,提高了扩展性,符合开闭原则(对扩展开放,对修改关闭)。

public class Person{
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参传入子类对象
this.pet = pet;
}
public void feed(){
pet.eat(); //pet 实际引用的对象类型不同,执行的 eat 方法也不同。
}
}
...
Person person = new Person();
Dog dog = new Dog();
person.adopt(dog);//实参是 dog 子类对象,形参是父类 Pet 类型

**弊端:**使用父类的引用,无法直接访问子类特有的属性和方法。

使用了多态后,==同名属性则会使用父类的,子类独有的属性无法使用,子类新定义的方法无法使用,只有重写过的方法能使用。==

只有先向下转型,才能使用子类的属性或新定义的方法。

根据左边的引用类型,决定调用父类的属性还是子类的属性,而同名方法是调用的子类对象重写过后的方法。

Base b = new Sub(); //引用类型是 Base,调用Base的属性,即 1。
System.out.println(b.a);
Sub s = new Sub(); //引用类型是 Sub,调用 Sub 的属性,即 2。
System.out.println(s.a);
class Base{
int a = 1;
}
class Sub extends Base{
int a = 2;
}

1.12.1 虚方法调用(Virtual Method Invocation)#

Java 中的虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定实际被执行的方法。

编译时,按照对象名的引用类型处理,只能调用父类中有的属性和方法,不能调用子类==特有的==变量和方法。但运行时,按照对象本身的类型处理,执行的方法是子类重写过后的方法。

引用类型 对象名 = new 对象();

class Base{
void express(){
System.out.println("我是Base");
}
}
class Sub extends Base{
@Override
void express() {
System.out.println("我是Sub");
}
}
Base b = new Sub(); //引用类型是 Base
b.express(); //按右侧的Sub类型执行方法,打印"我是Sub"
Sub s = new Sub(); //引用类型是 Sub
s.express(); //按右侧的Sub类型执行方法,打印"我是Sub"

1.13 向下转型#

子类对象由父类引用类型==>子类引用类型

父类对象不存在向下转型。

向上转型是多态。

1.13.1 instanceof#

引用类型 a = new 对象X();

a instanceof 类A :对象 a 为类 A 或其子类创建的对象 ,则返回 true。

  • 对象 a 的引用类型要么是类 A,要么与类 A 有继承关系(无论是类 A 的父类引用类型还是子类引用类型),才能过编译。

    下面的例子也证明了编译是看引用类型。

    //Base是父类,sub和sub1是两个子类。
    Sub b = new Sub(); //声明的引用类型为Sub
    System.out.println(b instanceof Sub1); //这里会报错,因为Sub和Sub1没有继承关系。
    Base b1 = new Sub(); //声明的引用类型为Base
    System.out.println(b1 instanceof Sub1); //不会报错,返回值为false

向下转型

if(pets[i] instanceof Dog){ //
Dog dog = (Dog) pets[i];
dog.watchHouse(); //Dog 特有的方法
}

1.14 Object 类#

如果一个类没有显式继承父类,那么默认则继承 Object 类。

Object 类没有属性,只有方法。

1.14.1 clone()#

克隆出一个新对象

步骤:

  1. 被克隆的类实现Cloneable接口
  2. 通过对象.clone()产生一个新对象。

会抛出异常,需要try...catch接收。

@Test
public void test(){
Animal a1 = new Animal();
try {
Animal a2 = (Animal)a1.clone();
System.out.println(a1==a2); //false,
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
//因为 clone() 的返回值为 Object,这里需要强转,a2 和 a1 是 2 个不同的对象。
class Animal implements Cloneable{ //需要实现Cloneable接口
int age =10;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

1.14.2 finalize()#

当 GC(Garbage Collection,垃圾回收器)要回收对象时,可以调用此方法。在子类中重写此方法,可在释放对象前进行某些操作。

在 JDK 9 中此方法已经被标记为过时的。

1.14.3 getClass()#

获取对象的运行时类型

Base sub = new Sub();
System.out.println(sub.getClass());//输出为:class com.lc.Sub

1.14.4 equals()[重要]#

Objectequals() 相当于 “==”。

==”对于基本数据类型(会自动类型提升),比较是否相等;而对于引用数据类型,比较地址值是否相等。

对于引用数据类型,使用“==”时,对象名对应的类型要么相同,要么有继承关系,才能过编译(和instanceof一个要求)。

格式:obj1.equals(obj2)

所有类都继承了 Object,也就获得了 equals()方法,并且可以重写。

File、String、Date 及包装类重写了 equals()方法,比较的是==引用类型及内容==,而不是地址值。

类有属性时,按 Alt+ Insert 可以快速重写。没有属性时,重写没有实际效果。

1.14.5 toString()[重要]#

打印引用数据类型变量,默认会调用 toString()

在自定义类,没有重写 toString() 的情况下,打印的是“对象的运行时类型@对象的hashCode值的十六进制形式

System.out.println(base); //com.lc.Base@1b6d3586

可以根据用户需求,重写 toString()方法,例如 String 类重写了 toString() 方法,可以返回字符串的值。

@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age +'}';
}

1.15 static#

成员变量按照是否使用 static 可以分为:

  1. 使用:静态变量(也被称为类变量)。

    1. 随类加载而加载,内存空间里就一份(因为类只加载一次),被类的所有对象共享。jdk6 放在方法区,jdk7 及以后放在堆空间。

    2. 可以通过对象调用。

  2. 不使用:非静态变量或实例变量。

    1. 每个对象拥有一份实例变量,随对象的创建而加载,存放在堆空间的对象实体中。
    2. 只能通过对象调用。

static 修饰的方法:类方法、静态方法。

特点:

  1. 随类加载而加载,可以通过对象调用。

  2. 静态方法内可以使用静态变量和其他的静态方法,不可以调用非静态的属性和方法。

  3. 静态方法中不能使用 this.super.,静态方法在创建对象之前就能使用,所以不能有this.super.

public class ChildTest {
public static void main(String[] args) {
Base base = null;
System.out.println(base.a);
//这里会输出1,因为静态变量属于类,和具体的对象无关,即便该对象是null。。
}
}
class Base{
static int a = 1;
}

类的实例对象可以作为类的属性。

JVM 把类 A 的模板加载进内存后,执行到A a = new A();时,发现内存中有类 A 的模板,则能过编译且成功执行。

class A {
static A a = new A(); //随类加载而加载,只执行一次。以后使用时,就去对应存储位置找现成的。静态变量a里也有属性a,只不过指向的是自己。
}

1.16 native 关键字#

当一个方法被 native 修饰时,说明这个方法是用非 Java 的编程语言写的。

1.17 类成员-代码块(初始化块)#

public class A{
int a = 10;
public A(){
}
{
... //非静态代码块
}
static{
... //静态代码块
}
}

静态代码块和非静态代码块的相同点

  1. 用于初始化类的信息。

  2. 内部可以声明变量、调用属性或方法、编写输出语句等操作。

  3. 如果声明多个代码块,则按照声明的先后顺序(即在类中,谁在上面,谁先执行)执行。

    静态代码块的执行总先于非静态代码块的执行。

    不同点

  4. 静态代码块==随类的加载而执行==,只执行一次;非静态代码块==随对象的创建而执行==,即调用构造器时执行,可执行多次。

    理解:

    1. 可以把“非静态代码块”当作类中直接调用父类的那些构造器的一部分,并放在那些构造器的前几行(但在父类的构造器后),一调用构造器就先执行非静态代码块。

    2. 若调用的构造器使用了this(...),非静态代码块不看成这些构造器的一部分,而看成在this(...)里面,直到找到那个直接调用父类的构造器,将代码块视为其一部分。

    3. 子类的构造器都会直接或间接调用父类的构造器,所以父类的非静态代码块也会执行,且父类构造器比子类构造器先执行。

    public A(){ //执行的时候可以理解成这样
    [super();] //父类构造器,可以不写,默认调用父类空参构造器。
    {
    ... //非静态代码块
    }
    System.out.println("我是直接调用父类的构造器");
    ...
    }
    public A(int a){
    this(); //使用了this(),则没有调用父类的构造器,属于间接调用,因为A()中调用了父类构造器。
    System.out.println("我不是直接调用父类的构造器");
    }

    示例:

    Base base = new Base(1,"lc");
    /*
    new Base(1,"lc")调用的是Base(int b, String name)构造器,其中又调用了this(b),即public Base(int b)。
    public Base(int b)又调用this(),即public Base()。
    public Base()没有显式给出调用父类构造器,则默认调用父类空参构造器。
    从以上描述可以看出,public Base()是类中直接调用父类的构造器,非静态代码块可以看为它的一部分,但是在父类的构造器之后。
    public A()是类中直接调用其父类Object的构造器,而Object没有任何输出,然后执行类A的非静态代码块,再执行类A构造器的其余代码,执行完再执行Base的非静态代码块,再执行Base()的其余代码以及其他构造器的代码。
    */
    /*
    结果为:
    我是代码块A
    我是构造器D
    我是代码块Base
    我是构造器A
    我是构造器B
    我是构造器C
    */
    class Base extends A{
    int b = 1;
    String name;
    {
    System.out.println("我是代码块Base");
    }
    public Base(){
    System.out.println("我是构造器A");
    }
    public Base(int b) {
    this();
    this.b = b;
    System.out.println("我是构造器B");
    }
    public Base(int b, String name) {
    this(b);
    this.name = name;
    System.out.println("我是构造器C");
    }
    }
    class A{
    {
    System.out.println("我是代码块A");
    }
    public A() {
    System.out.println("我是构造器D");
    }
    }
  5. 静态代码块内部只能调用静态的属性或方法,不能调用非静态的属性、方法;非静态代码块中非静态的或静态的属性、方法都可以调用。

非静态代码块的意义:如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。

静态代码块的意义:当静态变量的初始化很复杂时,可以使用静态代码块。

复杂初始化

给实例属性赋值的执行先后次序:

  1. 默认值

  2. 显式初始化或代码块初始化(谁放上边,谁先执行,且父类构造器执行完成后,子类中的成员变量才会开始显式初始化。)

    class A{
    {
    a = 20; //先代码块初始化,再显式初始化,最终a的值为10。
    }
    int a = 10; //比较神奇的是,可以先赋值,再定义变量。
    }
    class A{
    int a = 10;
    {
    a = 20; //先显式初始化,再代码块初始化,最终a的值为20。
    }
    }
  3. 构造器中初始化

  4. 通过“对象.属性”赋值

1.18 final#

final 代表最终的,即修饰的内容不能修改。

final 能够修饰的结构:类、方法、变量。

  • final 修饰类:该类无法被继承。

  • final 修饰方法:该方法无法被重写。

  • final 修饰变量(成员变量和局部变量都行),一旦赋值,就无法更改。

    • 成员变量赋值(==必须初始化==)

      • 显式赋值

      • 代码块中赋值

      • 构造器中赋值

    • 局部变量赋值

      • 方法内的局部变量,调用之前必须赋值。
      • 方法的形参,调用时传给形参的值无法更改。
  • finalstatic 一起修饰的成员变量,称为全局常量。

final 修饰引用类型变量时,其指向的对象的物理地址不能变。即不能再给这个变量赋另外的对象,但对象里面的属性值能修改。

对象的运行时类型@对象的hashCode值的十六进制形式”不是该对象物理地址。

1.19 abstract#

abstract 代表抽象的,可以修饰类和方法。

  • 修饰类:该类为抽象类,不能实例化。==可以包含构造器==(抽象类的构造器不能被直接调用,子类创建对象间接会调用抽象类的构造器,初始化父类成员变量。)

    1. 抽象类也是类,可以有属性、非抽象方法,代码块。

    2. 抽象类也可以继承抽象类。

  • 修饰方法:只有方法的声明,没有方法体。无需具体实现,实现是子类的事。

    public abstract void eat(形参); //抽象方法声明,没有“{}”。
    1. 有抽象方法的类一定是抽象类,没有抽象方法的类也可以是抽象类。

    2. 子类必须重写完所有抽象方法才能实例化。

可以创建引用类型为抽象类的数组,但存放的元素必须是,继承了抽象类重写完所有抽象方法的类所创建的实例。

Human[] human = new Human[3];
human[0] = new Chinese();
human[1] = new American();

1.20 接口#

1.20.1 接口的定义#

接口是一种规范。想要有某种功能,实现对应的接口就行。类和接口是“has-a”的关系,接口和类不是继承关系,而是实现关系。一个类可以实现多个接口。

关键字:interface

特点:

  1. 属性:

    接口的属性都是公共静态的常量,默认用 public final static 修饰,可以省略不写。

  2. 方法

    • jdk7及以前只能声明抽象方法,且默认用public abstract修饰,可以省略不写。

    • jdk8以后可以使用

      • 静态方法,public static 返回值 method(){...},可以省略 public。==接口中的静态方法只能用接口调用==,不能由其实现类调用。

        接口名.静态方法名();

      • 默认方法,public default 返回值 method1(){...}default不能省略,可以省略 public

        1. 默认方法也可以被重写,且在类中default可以被省略,并需要补上public
        2. 若想在被重写的默认方法中保留接口的默认方法,用接口名.super.方法名();
    • jdk9后可以使用私有方法,以供默认方法调用:private 返回值 method(){...}

  3. 不可以声明构造器、代码块(类才有这些)。

  4. 类必须重写接口中所有的抽象方法,否则必须声明为抽象类。

  5. 接口可以继承接口,且可以多继承。interface cc extends AA,BB{}

  6. 接口也有多态,也可以作函数的形参,接口名 base = new 实现类();,只能调用接口有的方法,不能调实现类独有的方法。

    在接口和抽象类的方法中,若使用了实现类新增的属性,用了多态也不会报错。

    //以接口为例
    public class A {
    public static void main(String[] args) {
    I b = new B(); //多态,以接口的引用类型调用实现类重写的方法
    System.out.println(b.getA()); //abc,没有因为接口里没有属性a而报错
    }
    }
    class B implements I{
    String a = "abc"; //属性a是B独有的
    @Override
    public String getA() {
    return a;
    }
    }
    interface I {
    String getA();
    }
  7. 可以创建引用类型为接口的数组,但存放的元素必须是,重写完所有抽象方法的实现类所创建的实例。

    Eatable[] eatable = new Eatable[3];
    eatable[0] = new Chinese();
    eatable[1] = new American();

1.20.2 冲突问题#

方法冲突:

  • 父类优先原则

    当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类默认执行父类的成员方法

    几种情况:

    1. 若不重写,默认保留父类的方法
    2. 在重写的方法里,调用父类的方法,super.方法();
    3. 在重写的方法里,调用接口的默认方法,接口名.super.方法();
    4. 完全重写

属性冲突:

  • 父类的属性与接口属性重名。

    解决方法:

    1. super.属性:父类属性
    2. 接口名.属性:接口属性(因为接口里的属性是static的,所以可以通过接口名调用)
image-20220328002053452

1.21 内部类(InnerClass)#

将一个类 A 定义在另一个类 B 里面,里面的那个类 A 就称为内部类(InnerClass),类 B 则称为外部类(OuterClass)

当一个事物 A 的内部的某部分是一个完整结构 B ,而结构 B 又只为事物 A 提供服务,不在其他地方单独使用,那么完整结构 B 最好使用内部类。

1.21.1 内部类的分类#

image-20221124223912529

成员内部类也是类的一种:

  • 可以在内部定义属性、方法、构造器等结构。
  • 可以继承,可以实现接口。
  • 可以声明为 abstract 类 ,因此可以被其它的内部类继承。
  • 可以声明为 final 的,此时不能被继承。

成员内部类看成外部类的成员:

  • 内部类可以调用外部类的属性或方法(静态内部类不能使用外部类的非静态成员)。
  • 成员内部类四种权限修饰符都能用,而外部类只能用 public 与缺省修饰符。
  • 可以用 static 修饰。
class Person{
//静态成员内部类
static class Dog{
}
//非静态成员内部类
class Bird{
//若出现同名变量或方法,就近原则。想使用外部类的,需用Person.this.变量(方法)名。
//this.变量(方法)名,就近原则。
}
public void method(){
//局部内部类
class InnerClass1{
}
}
public Person(){
//局部内部类可以重名,不能有权限修饰符
class InnerClass1{
}
}
}
//在main()创建静态内部类实例,若在外部类中创建内部类的实例,就和之前创建对象一样。
Person.Dog dog = new Person.Dog();
//在main()创建非静态内部类实例,非静态内部类需要外部类的对象来创建。
Person p1 = new Person();
Person.Bird bird = p1.new Bird(); //也可以写成Bird bird = p1.new Bird();

匿名内部类:

new 父接口(){ //创建了实现类的一个对象
重写方法...
}.方法(方法的参数);
new 父类(构造器参数){ //创建了子类的一个对象
重写方法...
}.方法(方法的参数);

举例:

public class Demo {
public static void main(String[] args) {
new B(){ //通过匿名内部类的对象直接调用方法。
@Override
public void a() {
...
}
}.a();
B b = new B(){ //多态
@Override
public void a() {
...
}
};
b.a(); //通过父接口的引用调用方法
}
}
interface B{
void a();
}

匿名内部类作为参数。

interface A{
void method();
}
public class Test{
public static void test(A a)
{
a.method;
}
public static void main(String[] args) {
test(new A(){
@Override
public void method() {
System.out.println("a");
}
});//此时匿名内部类是一个参数
}
}

1.22 枚举类#

枚举类型本质上是一种类,只不过是这个类的实例是有限的、固定的几个,不能让用户随意创建。比如:春、夏、秋、冬。

若枚举只有一个对象, 则可以作为一种单例模式的实现方式。

enum Season{ //默认继承了 Enum 类,不能再显式继承其他类,但可以实现接口。
//1.必须在枚举类开头声明多个对象,对象直接用“,”隔开,省略了修饰符“public static final”。
SPRING("春天","春暖花开"),
//可以看成“public static final SPRING = new Season("春天","春暖花开");”,但是不能这么写
SUMMER("夏天","夏日炎炎"),
AUTUMN("秋天","秋高气爽"),
WINTER("冬天","白雪皑皑");
//2.声明描述枚举类的属性,用“private final”修饰。
private final String seasonName;
private final String seasonDesc;
Season(String seasonName, String seasonDesc) { //构造器修饰符省略了“private”
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}
//通过“枚举类名.常量”获取枚举对象,如 Season.SPRING
System.out.println(Season.SPRING.toString()); //默认打印对象名,SPRING,可以重写
System.out.println(Season.SPRING.name()); //打印对象名,SPRING
Season[] values = Season.values(); //所有枚举类对象放到 values 数组中
Season.valueOf("SPRING"); //返回字符串同名的枚举对象,没有相应的枚举对象则报错。

枚举类实现接口的 2 种方式

enum A implements 接口1,接口2{ //共用一个重写后的方法。
//抽象方法的实现
}
//2、枚举类的常量可以继续重写抽象方法,每个枚举对象有自己的重写方法。
enum A implements 接口1,接口2{
常量名1(属性1,属性2..){
//抽象方法的实现或重写,实现方式是匿名内部类
},
常量名2(属性1,属性2..){
//抽象方法的实现或重写
},
//...
}

1.23 注解(Annotation)#

注解是给编译器或其他程序读取的。程序可以根据不同的注解,做出相应的处理。

1.23.1 元注解#

元注解是加在其余注解上,对其余注解进行说明的注解。

(1)**@Target:**用于描述注解的使用范围。

  • 可以通过枚举类 ElementType 的 10 个常量对象来指定,如 TYPEMETHODCONSTRUCTORPACKAGE等。

    例子:@Target({ElementType.METHOD})

(2)**@Retention:**用于描述注解的生命周期

  • 通过枚举类型 RetentionPolicy 的3个常量对象来指定。
    • SOURCE:编译的时候就丢弃,字节码文件里没这个注解。
    • CLASS:被保留在字节码文件中,但在 JVM 运行的时候不会保留。
    • RUNTIME:JVM 运行时也保留。
  • 只有 RUNTIME 才能被反射读取到。

(3)@Documented:表明这个注解应该被 javadoc 工具记录,也就是文档注释生成的那个网页会保留这个注解。

文档注释1

(4)**@Inherited:**允许子类继承父类中的注解,也就是父类的注解,子类默认拥有,无需显式写出来。

1.23.2 自定义注解#

格式如下:

【元注解】
【修饰符】 @interface 注解名{
【成员列表】
}
  • Annotation 的成员在 Annotation 定义中以无参数有返回值抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。

  • 可以使用 default 关键字为抽象方法指定默认返回值。

  • 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名value

例子:

@Target({METHOD})
public @interface myAnnotation{
String value() default "hello"; //default "hello" 可加可不加,指定默认返回值
}
...
@myAnnotation(value="world")
public void method1(){
}
@myAnnotation() //使用默认返回值,"hello"
public void method1(){
}

1.23.3 JUnit 单元测试#

一种白盒测试,需要导入 jar 包。导入后,可以使用 @Test 注解,使运行代码无需 main() 方法。

要求:

  • 所在的类必须是 public,非抽象的,只有无参构造器。

  • @Test 标记的方法是 public,非抽象的,非静态的,void无返回值的,()无参数的。

public class Person {
@Test
public void test1(){
System.out.println("hello");
method(10); //需要测试的方法写里面,并且无需通过对象调用。
}
public int method(int num){
return num;
}
}

1.24 包装类#

包装类包装类对比

装箱:把基本数据类型转为包装类对象。

拆箱:把包装类对象拆为基本数据类型。

有自动装箱和自动拆箱。

Integer i = 1; //自动装箱
//底层调用 Integer i = Integer.valueOf(1);
i = i + 5; //等号右边:将 i 对象转成基本数值(自动拆箱) i.intValue() + 5; 加法运算完成后,再次装箱,把基本数值转成对象。
//只能与自己对应的类型之间才能实现自动装箱与拆箱。
Double d = 1;//错误的,1 是 int 类型,没有自动类型提升,过不了编译。

1.24.1 基本数据类型与字符串的转换#

基本数据类型=>字符串:(前者转换为后者,去后者找方法)

**方式 1:**调用字符串重载的 valueOf() 方法

int a = 10;
//String str = a;//错误的
String str = String.valueOf(a);

**方式 2:**更直接的方式

int a = 10;
String str = a + "";

字符串=>基本类型与包装类:(前者转换为后者,去后者找方法,基本数据类型没有方法,只能去包装类)

//除了 Character 类之外,可以使用 parseXxx()。parseXxx()是静态方法,返回值为包装类。但由于自动拆箱,可以赋值给基本数据类型。
int a = Integer.parseInt("123");

1.24.2 包装类缓存对象#

自动装箱在一定范围内,相同的值使用的是同一个对象,不会新创建对象。

包装类缓存范围
Byte-128~127
Short-128~127
Integer-128~127
Long-128~127
Character0~127
Booleantrue 和 false
//自动装箱的底层调用的是 Integer 的 valueOf
Integer a = 1; //自动装箱,在一定范围内,相同的值使用的是同一个对象。
Integer b = 1;
System.out.println(a==b); //true
Integer a = new Integer(1); //2 个不同的对象
Integer b = new Integer(1);
System.out.println(a==b); //false
Integer i = 1000;
double j = 1000;
System.out.println(i==j);//true 会先将 i 自动拆箱为 int,然后根据基本数据类型“自动类型转换”规则,转为 double 比较。
Integer i = 1;
Double d = 1.0;
System.out.println(i==d);//编译报错,2个不同的包装类的比较不会自动拆箱。

一般情况下,包装类对象是“不可变”对象,改变数值后,会创建一个新对象。

特殊情况:在自动装箱(前提1)和一定范围(前提2)的前提下,修改数值后又改回原数值,使用的是同一个对象。

Integer b = new Integer(2); //没有满足前提1
Integer c = b;
b += 10;//等价于 b = new Integer(b+10);
b -= 10;
System.out.println(b==c); //false
Integer f = 1113; //没有满足前提2,不在缓存范围内
Integer c = f;
c += 10;
c -= 10;
System.out.println(c==f); //false
Integer f = 12; //满足了前提1和2
Integer c= f;
c += 10;
c -= 10;
System.out.println(c==f); //true

Java学习笔记三#

1.异常#

Java 把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象,并且抛出(throw),然后程序员可以捕获(catch)异常。

1.1 异常体系#

1.1.1 Throwable 类#

Throwable 是异常的根父类,在 java.lang 包下,继承了 Object

Throwable 有 2 个子类,ErrorException

Throwable中的常用方法:

  • public void printStackTrace():打印异常的详细信息。

    包含了异常的类型、异常的原因、异常出现的位置(报异常时的那堆红字)。

    printStackTrace

1.1.2 Error 类和 Exception 类#

Error:==Java 虚拟机无法解决==的严重问题。如:JVM 系统内部错误、资源耗尽等严重情况。==一般不编写针对性的代码进行处理==。

例如:StackOverflowError(栈内存溢出)和 OutOfMemoryError(堆内存溢出,简称OOM)。

Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。

例如: 空指针访问、试图读取不存在的文件、网络连接中断、数组角标越界。

1.1.3 编译时异常和运行时异常#

Java 程序的执行分为编译时过程和运行时过程。

  • 编译时异常(受检异常,Exception 子类中除了 RuntimeException 外都是编译时异常):写代码期间必须抛出异常,不然不给过编译,即便可能不会出现异常。

    例如,天气预报(编译器)告诉你可能下雨(出现异常),你不带伞(抛出异常)就不让你出门(过编译)。

    文件读写必须抛出异常,不然不给过编译。FileNotFoundExceptionIOException

  • 运行时异常RuntimeException及其子类,非受检异常):运行代码后出现的异常。编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。

    常见的运行时异常:

    常见的运行时异常

1.2 异常处理#

运行时异常,不针对性地处理,出现异常时再改代码;编译时异常需用以下的语句处理。

1.2.1 try-catch-finally#

try{
...... //可能产生异常的代码
//{}里定义的是局部变量,不能在其他的{}里使用
}
catch( 异常类型1 e ){ //e是异常类创建的对象的引用
...... //当产生异常类型1型异常时的处置措施
}
catch( 异常类型2 e ){
...... //当产生异常类型2型异常时的处置措施
}
finally{
..... //无论 try 或 catch 中是否发生异常,finally中的语句一定要执行。
//若try 或 catch 里有 return 语句,也需等 finally 里的语句执行完后才能 return。即便在finally 里修改了需要返回的变量的值,return 的值也是修改前的那个值,除非在 finally 再写个 return,抢先 return。
}
//catch()最多执行一个。若多个异常存在子父类关系,则子类放上面,父类放下面。
/*
catch(... e){
e.printStackTrace(); //打印异常信息,就是红色字体那个信息
System.out.println(e.getMessage()); // 返回值为 new 异常类名("需打印的字符串")里的字符串
}
*/

常用形式:try-catchtry-catch-finally

1.2.2 throws#

修饰符 返回值类型 方法名(参数) throws 异常类名 1,异常类名 2…{ }

throws 后面可以写多个异常类型,用逗号隔开。

public void readFile(String file) throws FileNotFoundException,IOException {
...
// 读文件的操作可能产生 FileNotFoundException 或 IOException 类型的异常
FileInputStream fis = new FileInputStream(file);
...
}

说明:

  1. 把异常抛给调用者,让调用者使用 try...catch 处理。

  2. main() 方法不要用 throws

  3. 针对编译时异常,子类重写的方法抛出的异常类型需小于等于父类被重写的方法抛出的异常类型。

  4. 父类的方法不 throws 异常,那么子类中重写后的方法也不能 throws 异常。

Father f = new Son();
try{
...
}catch(异常类型 e){ //这里的异常类型需写父类抛出的异常类型,因为编译看左边,把 f 当成了 Father 类型,但实际执行的是子类的方法,若子类抛的更大,则这里抓不住,也就处理不了。
...
}

1.2.3 throw#

根据实际开发需求,如果出现不满足具体场景的代码问题(如不能除负数),程序员主动使用 throw 关键字抛出异常对象。

格式:throw new 异常类名("需打印的字符串");

public String getMessage():可以得到这个字符串。

注意事项

  • 如果 throw 的是编译时异常类型的对象,同样需要使用 throws 或者try...catch 处理,否则编译不通过。

  • 如果是运行时异常类型的对象,编译器不提示,可以不用 throwstry...catch

  • throw 语句会导致程序执行流程被改变,throw 语句是明确抛出一个异常对象,因此它下面的代码将不会执行

1.3 自定义异常类#

步骤:

  1. 继承一个异常类型。自定义编译型异常继承Exception;自定义运行时异常继承RuntimeException
  2. 提供至少两个构造器,一个是无参构造,一个是参数为(String message)的构造器。
  3. 自定义异常需要提供 serialVersionUID

2.String 字符串#

2.1 基本概念#

String s1 = "hello";“hello”叫字符串常量,字符串常量存储在字符串常量池,字符串常量池不允许存放 2个相同的字符串常量。jdk7 之前,字符串常量池在方法区,jdk7 及以后字符串常量池在堆中。

String 对象的字符内容是存储在一个字符数组 value[] 中的。"hello" 等价于 char[] data={'h','e','l','l','o'}

//jdk8中的String源码:
public final class String //String类用final修饰,所以不能继承 String。
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; //String对象的字符内容是存储在此数组中
//jdk9中char数组改成了byte[]数组。

不可变的原因:

private final char value[];

  • private : 外部无法获取字符数组,且 String 没有提供 valuegetset 方法(外部不能改)。

  • final :引用不可变,且 String 也没有提供修改 value 数组某个元素值的方法(内部不能改)。

所以一旦创建了 String 对象就无法改变。赋值、拼接、replace() 看似改变了字符串的值,实际上是创建存储新值的新对象,而没有修改原对象的值。

2.2 字符串拼接:#

  • 常量 + 常量:结果仍存储在字符串常量池中,返回此字面量的地址。

    这里的常量指的是字面量或 final 修饰的常量(但不能是某个函数的返回值,即便返回值不变。如 final String s = B.get())。

    因为在编译期间就能确定拼接的结果,所以直接返回了字面量的地址。

    String s = "abc"+"def";
    final String s1 = "abc"; //s1 也看成常量
    String s2 = s1 + "def";
    System.out.println(s == s2); //true
  • 一旦拼接了变量,则会在堆中 new 一个对象,再指向字符串常量池中的字符串,返回的是堆空间对象的地址。

concat() 始终会在堆空间 new 一个对象,指向常量池的字面量。

2.3 常见的构造器#

  • public String() :创建一个新的 String 对象,使其表示空字符串。

    String s = new String(); //相当于String s = new String("");
  • String(String original): 创建一个新的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。

  • public String(char[] value) :通过字符数组来构造新的 String

  • public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新的 String

  • public String(byte[] bytes) :使用默认字符集解码参数中的字节数组来构造新的 String

  • public String(byte[] bytes,String charsetName) :使用“指定的字符集”解码参数中的字节数组来构造新的 String。

2.4 String 和 char[] 的转换#

String => char[]:调用 StringtoCharArray()

char[] arr = str.toCharArray();

char[] => String:调用 String 的构造器。

String s = new String(arr);

2.5 String 和 byte[] 的转换#

String => byte[]:调用 StringgetBytes()

byte[] arr1 = "我爱你".getBytes("字符集"); //不指定字符集则默认使用 utf-8

byte[] => String:调用 String 的构造器。

String s1 = new String(arr1,"字符集");//不指定字符集则默认使用 utf-8

编码:String => 字节数组(看得懂的 => 看不懂的)

解码:字节或字节数组 => String

编码和解码的字符集必须相同。

2.6 字符串常量池的理解#

感觉理解还是不到位,暂时像下图这样记,了解了虚拟机后再补充。

String的存储方式

String s2 = new String("hellolc");,会创建 2 个对象,一个是堆空间中 new 的对象,另一个是堆空间的字符串常量池中的 String 对象。

String s1 = new String("1") + new String("2");
//实际上是执行的:String s1 = StringBuilder().append("1").append("2").toString();
//以上代码只会在字符串常量池中放入"1"和"2",并不会在常量池中放入"12",记成结论就行。
//下图省略了 new 出来的2个字符串对象(new String("1")、new String("2"))。
String常量池的理解

s.inertn():如果在常量池中存在与 s 内容相同的字符串,就直接返回常量池中相应字符串的引用(情况1),否则在常量池中保存 s 的引用,并将 s 的引用返回(情况2)。

情况 2 的图(省略了常量池里的”111”、“222”):

String s1 = new String("111") + new String("222");
s1.intern();
String s2 = "111222";
System.out.println(s1==s2); //true,情况2
常量池里没有"111222",先把 s1 的引用放入常量池,s2 取到的实际上是 s1的引用。
String常量池理解1

情况 1 的图(省略了常量池里的”111”、“222”):

String s1 = new String("111") + new String("222");
String s2 = "111222";
s1.intern();
System.out.println(s1==s2); //fasle,情况1

常量池最初没有”111222”,String s2 = "111222"; ,将”111222”放入了常量池,再调用s1.intern();时,常量发现有了”111222”,则直接返回常量池中相应字符串的引用,由于没使用返回值,相当于什么也没做,所以 s1 和 s2 的引用不同。

String常量池2

2.7 字符串相关类之可变字符序列#

因为 String 对象是不可变对象,虽然可以共享常量对象,但是若频繁地修改或拼接字符串,会导致效率极低,空间消耗也比较高。

继承结构:

image-20220405174233055

AbstractStringBuilderjdk 9及以后改成 byte[]):

image-20220405174414780

StringBuilderStringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样,两者地主要区别是方法是否添加 synchronized,下面以 StringBuilder 为例。

2.7.1 StringBuffer(先提出)#

线程安全(方法有 synchronized 修饰),效率低;最初使用 char[] 数组存储,jdk 9及以后改成 byte[]

2.7.2 StringBuilder(后提出)#

线程不安全的,效率高;最初使用 char[] 数组存储,jdk 9及以后改成 byte[]

image-20220228153030902

常见构造器:

public StringBuilder() { //空参:默认容量为16
super(16);
}
public StringBuilder(int capacity) { //主动指定容量大小,避免多次扩容
super(capacity);
}
public StringBuilder(String str) { //传入字符串,容量为16 + 字符串的长度
super(str.length() + 16);
append(str);
}

扩容:不断地添加元素,一旦 count 要超过 value.length 时,就扩容,默认扩容为“原有容量的 2 倍 + 2”,并将原数组的元素复制到新数组中。

面试题StringBuffernull):

String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str); //底层会改成添加"null"字符串。
System.out.println(sb.length());//返回现有字符的个数,4
System.out.println(sb);// null
StringBuffer sb1 = new StringBuffer(str);
System.out.println(sb1);//空指针异常,StringBuffer、StringBuilder的构造器不能传 null。

返回值的说明:StringBuilder里的一部分方法的返回值为StringBuilder类型,实际上返回的是被操作对象本身(源码里返回的this),没有创建新对象。

StringBuffer同理。

3.Java 集合框架体系#

map、list、set 等集合中只能存放引用数据类型,而不能存放 int、double 等基本数据类型。若添加基本数据类型,则会先自动装箱。

3.1 Collection接口:存储一个一个的数据#

Collection接口是 ListSet 接口的父接口。Collection接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 集合。

3.1.1 使用建议#

使用 Collection 时,无论使不使用 hashCode()equals() ,都建议重写。

image-20220407203244029

3.1.2 常用方法#

//尾部增加
add(Object obj)
add(Collection coll) //把 coll 中的元素依次加入,而不是把 coll 看成整体加入
//删除
clear() //清空集合元素
remove(Object obj) //移除列表指定位置的一个元素。
removeAll(Collection coll) //从当前集合中删除所有与 coll 集合中相同的元素
retainAll(Collection coll) //仅保留两个集合的交集
//判断
size()
isEmpty()
contains(Object obj)
containsAll(Collection coll) // coll 集合是否是当前集合的“子集”
//遍历
iterator():使用迭代器
增强 for 循环
一般的 for 循环

集合 => 数组:调用集合的 toArray() 方法。

toArray有两个重载的方法:

  • list.toArray();list直接转为Object[] 数组,无法将Object[] 强转为具体类型的数组。
  • list.toArray(T[] a);list转化为自定义类型的数组,此方法只适用于引用类型的数组,int[]会报错。

例子:

List<int[]> merged = new ArrayList<int[]>();
merged.toArray(new int[merged.size()][]);
//将merged转为二维数组,这里的merged.size()也可以用0替代。
//因为当参数数组的length小于arraylist的size()时,二维数组的大小取arraylist.size()。
//若大于,则取参数数组的length。

数组 => 集合:调用 Arrays 的静态方法 asList(Object...objs)

Integer[] arr1 = new Integer[]{1,2,3};int[] arr2 = new int[]{1,2,3};arr1arr2 作为 asList 的参数时有区别。Arrays.asList() 不支持基本数据类型的数组,它只能接受 Object 类型的参数。如果传入一个基本数据类型的数组,Arrays.asList() 会把整个数组当成一个 Object 类型的实参,而不是把数组的每个元素都看成一个 Object 。返回的 List 只有一个元素。

Collectioncontains()/remove():根据对象里的 equals() 方法,来确定集合中有没有某个元素。不重写 equals() ,比较地址值;重写 equals() ,比较内容。

3.1.3 List 子接口#

存储有序的可重复的数据(“动态”数组)。

List 除了从 Collection 继承的方法外,还拥有根据索引来操作元素的方法。

常用方法

- 插入元素
- void add(int index, Object ele):在index位置插入ele元素
- boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
- 获取元素
- Object get(int index):获取指定index位置的元素
- List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
- 获取元素索引
- int indexOf(Object obj):返回obj在集合中首次出现的位置
- int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
- 删除和替换元素
- Object remove(int index):移除指定index位置的元素,将该元素后面的元素们往前移动一位。,并返回此元素
- Object set(int index, Object ele):设置指定index位置的元素为ele
1、ArrayList(常用)#

线程不安全、效率高;底层使用 Object[] 存储。

ArrayList底层

ArrayList<String> list = new ArrayList<>(20); //也可以通过构造器指定初始数组长度

常用方法:

//尾部增加
add(Object obj)
add(Collection coll)//把 coll 中的元素依次加入,而不是把 coll 看成整体加入
//删除
remove(Object obj)
remove(int index)
/* remove(2),删的是索引为 2 的元素,并把后面的元素向前移动1位。
remove(Integer.valueOf(2)),删除的才是值为 2 的元素
*/
//改
set(int index,Object ele)
//查,通过下标访问
get(int index)
//插
add(int index,Object ele)
add(int index,Collection ele)
//长度
size()
//遍历
iterator():使用迭代器
增强 for 循环
一般的 for 循环
2、Vector(不常用,早期实现类)#

线程安全、效率低;底层使用 Object[] 存储。

Vector底层

3、LinkedList#

线程不安全;底层使用双向链表存储,当需要频繁删除和插入数据时,建议使用此类。

LinkedList底层

除了从List接口继承的方法外,LinkedList还有专属的方法

  • void addFirst(Object obj)
  • void addLast(Object obj)add 方法的参数若是Object,相当于就是 addLast()
  • Object getFirst()
  • Object getLast()
  • Object removeFirst()
  • Object removeLast()

3.1.4 Set 子接口#

存储无序的、不可重复的数据。

无序性:元素具体的存储位置是由元素的 hashCode() 的返回值决定,每个元素并不是依次紧密存放的。

不可重复性:hashCode()equals() 都一样,则认为元素相同。

HashSetLinkedHashSet 必须重写 hashCode()equals() ,可以用 IDEA 的自动生成。

开发中的场景:过滤重复数据。

Set 的方法为 Collection 声明的抽象方法,没有新增的方法。

1、HashSet(常用)#

底层是用 HashMap 实现的。

特点:

  1. 元素可以是 null
  2. 不能保证元素的排列顺序。

HashSet 判断两个元素相等的标准:两个对象通过 hashCode() 方法得到的哈希值相等,并且两个对象的 equals() 方法返回值为 true

HashSet中添加元素的步骤:

  1. 当向 HashSet 中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法得到该对象的哈希值,然后将哈希值传入某个散列函数,计算出该对象在 HashSet 底层数组中的存储位置。

    哈希值不同也可能放到同一个位置。

  2. 如果存储位置上没有元素,则直接添加成功。

    若是在第2步添加成功,元素会保存在底层数组中。

  3. 如果存储位置上有元素,则继续比较:

    • 如果两个元素的哈希值不相等,则添加成功;
    • 如果两个元素的哈希值相等,则会继续调用 equals() 方法:
      • 如果 equals() 方法结果为 false,则添加成功。
      • 如果 equals() 方法结果为 true,则添加失败。

    若是在第3步添加成功,由于数组的对应位置已有元素,则以链表的形式添加。

2、LinkedHashSet#

Hashset 的子类,底层使用的是 LinkedHashMap

在“数组+单向链表+红黑树”的基础上,又添加了一组双向链表,用于记录添加元素的先后顺序,==使得遍历时,可以按照添加元素的顺序显示==。

LinkedHashSet 插入性能低于 HashSet,适用于需频繁查询的场景。

通过链表能够很快找到下一个元素,所以查得快。

3、TreeSet#

TreeSetSortedSet 接口的实现类。

底层使用“红黑树”存储。

可以按照元素的指定属性的大小顺序进行遍历,所以需要实现 ComparableComparator

说明

  • 添加到 TreeSet 中的元素必须是同一个类的对象,否则报 ClassCastException

  • TreeSet ==判断数据相同的标准==:compareTo()compare()的返回值为 0,所以无需重写 hashCode()equals()

  • 由于 TreeSet 不能存放相同的元素,则后一个相等的元素就不能被添加到 TreeSet 中。

  • 定制排序的实现类写在构造器的括号里,

    TreeSet<Employee> set = new TreeSet<>(comparator);

3.2 Map:存储一对一对的数据(key-value)#

image-20220409001015034

HashMapLinkedHashMap 都需要重写hashCode()equals()

3.2.1 key-value的特点#

image-20220409001213720

HashMap为例,HashMap中存储的keyvalue的特点如下:

  1. Map 中的 keySet 来存放,不允许重复。

  2. keyvalue 构成一个 entry。所有的 entry 彼此之间无序的、不可重复的,又构成了一个新的 Set

    image-20220514190412763

3.2.2 常用方法#

//增加
put(Object key,Object value) //返回值为修改之前的 value,若第一次加入则返回 null
putAll(Map m)
//删除
Object remove(Object key) //返回值为 value
//改
put(Object key,Object value)
putAll(Map m)
//通过key查value,没查到则返回null
Object get(Object key)
//长度
size()
//遍历
遍历 key 集:Set KeySet()
遍历 value 集:Collection values()
遍历 entry 集:Set entrySet()
然后再用迭代器或增强 for 循环
System.out.println("所有的key:");
Set keySet = map.keySet();
for (Object key : keySet) {
System.out.println(key);
}
System.out.println("所有的value:");
Collection values = map.values();
for (Object value : values) {
System.out.println(value);
}
System.out.println("所有的映射关系:");
Set entrySet = map.entrySet();
for (Object mapping : entrySet) {
//System.out.println(entry);
Map.Entry entry = (Map.Entry) mapping;
System.out.println(entry.getKey() + "->" + entry.getValue());
}

3.2.3 实现类#

1、HashMap(常用)#
  1. 线程不安全,效率高;
  2. 可以添加 nullkeyvalue 值;
  3. 底层使用“数组+单向链表+红黑树”存储,元素的存取顺序不能保证一致。

HashMap 判断两个 key 相等的标准是:两个keyhashCode 值相等,且equals()方法返回 true。

HashMap 判断两个 value 相等的标准是:两个 value 通过equals()方法返回 true

HashMap1

HashMap2

HashMap3

Jdk8的HashMap

2、LinkedHashMap#

LinkedHashMapHashMap 的子类。

在“数组+单向链表+红黑树”的基础上,又添加了一组双向链表,用于记录添加元素的先后顺序,使得遍历时,可以按照添加元素的顺序显示

频繁遍历的时候使用 LinkedHashMap

3、TreeMap#
  1. 底层使用“红黑树”存储;
  2. 可以按照添加的 key-value 中的 key 大小顺序进行遍历,需要实现 ComparableComparator

说明:

  • 添加到 TreeMap 中的 key 的类型必须一样,否则报 ClassCastException

  • TreeSet 判断数据相同的标准:compareTo()compare()的返回值为 0,所以无需重写 hashCode()equals()

  • key 如果是自定义对象,只要比较的内容相同,即便其余属性不同,也会被认为是同一个 key。例如,仅比较 key 的年龄,Person("Bob",23)Person("Alice",23) 就会因年龄相同,被认为是同一个 key

4、Hashtable(不常用,早期实现类)#
  1. 线程安全,效率低;
  2. 不可以添加 nullkeyvalue 值;
  3. 底层使用“数组+单向链表”存储。
5、Properties#
  1. PropertiesHashtable 的子类。
  2. keyvalue 都是 String 类型。常用来处理“.properties”属性文件。
  3. 存取数据时,建议使用setProperty(String key,String value)getProperty(String key)

properties文件的内容:

#key=value,键值对形式,用等号连接
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=123456
url=jdbc:mysql://127.0.0.1:3306/atguigu

3.3 迭代器#

Iterator接口的常用方法如下:

  • public E next():返回迭代的下一个元素。

  • public boolean hasNext():如果仍有元素可以迭代,则返回 true。

  • iterator.remove():删除iterator.next()指向的那个元素。

    image-20220407235130988

Collection c = new ArrayList();
c.add("a");
c.add(123); //自动装箱
Iterator iterator = c.iterator();
while(iterator.hasNext()){
//iterator.next(),先指针下移,再取元素。所以开始时,指针相当于在下标为 -1 的位置。
System.out.println(iterator.next());
}

3.4 增强 for 循环#

for(元素类型 obj : c){ //对于集合,底层调用了迭代器,c是Collection集合或数组
System.out.println(obj);
}
for(int value : arr){ //取出来的是数组的值,不是下标
System.out.println(value);
}

3.5 Collections#

Collections 是一个操作 SetListMap 等集合的工具类。

4.泛型#

4.1 泛型的定义#

泛型的定义

类型实参只能是引用数据类型。

4.2 引入泛型的原因#

不引入泛型可能出现的问题

image-20220411001522636image-20220411001549747

加上泛型后,方法的实参的引用类型只能是“<>”里的那种类型。没加之前,则认为操作的是 Object 类型的对象。

4.3 泛型的写法#

List<Integer> list = new ArrayList<>(); //第二个“<>”里的类型可加可不加
HashMap<String,Integer> map = new HashMap<>();

4.4 自定义泛型类/接口#

泛型要使用一路都用。要不用,一路都不要用,否则报错。

在造对象或继承时,指定泛型的类型。

在静态方法中,不能使用类型为泛型的属性,因为在造对象时才能确定泛型。

在声明完自定义泛型类以后,可以在类的内部(比如:属性、方法、构造器)使用类的泛型。

class Person<T>{
...
}
interface B<T1,T2>{
...
}
class Sub extends Person<Integer>{ //Sub 不是泛型类,因为其后面没有“<>”。
...
}
class Sub1<T> extends Person<T>{ // Sub1 是泛型类,父类里不确定的东西在子类里还是不确定
...
}
class Sub2<T,E> extends Person<T>{
//Sub2 是泛型类,父类里不确定的东西在子类里还是不确定,并且还多了不确定的E
...
}
class Sub3 extends Person{
...
}
//Person<Object>不等同于Person,比如形参为Person<String>,实参传入Person,不会报错;而传入Person<Object>,会报错。
//Person没有指明泛型参数类型,那就相当于没用泛型,可以表示所有对象。
//若泛型指定为Object,就只表示Object,不能表示所有对象。

4.5 自定义泛型方法#

如果我们定义类、接口时没有使用<泛型参数>,但是某个方法形参类型不确定时,这个方法可以单独定义<泛型参数>。

静态方法可以是泛型方法。

泛型方法所在的类不一定是泛型类。

**第一种泛型方法:**形参带泛型,返回值也可带泛型。

public <E> E method(E e){ //判断泛型方法是看返回值前面有没有“<E>”
//不加“<E>”,则可能把 E 当成一个类;通常在形参列表或返回值会出现泛型;
//调用方法时,通过传入的数据类型确定,E 是什么类型。
...
}

第二种泛型方法:只有返回值带泛型。

public <T> T method(String name){
Class<?> aClass = Class.forName(name);
Object o = aClass.newInstance();
return (T)o;
}
返回值类型 m = 对象.method("com.lc.file1.Person");
//这时候根据接收类型确定T,比如用m是Object类型,则T代表Object类型;m是Person类型,则T代表Person类型。如果o不能强转为m所代表的引用类型,则报ClassCastException。

4.6 泛型在继承上的体现#

  • 泛型为父类的集合可以存入子类的对象(多态)。

    List<Number> numbers = new ArrayList<Number>();
    numbers.add(new Integer(10));
  • 假设 SuperAA 的父类,G<A>的对象不能赋给G<SuperA> 的引用,因为两者内部结构都不同,是并列关系。

    G<SuperA> a = null;
    G<A> b = new G<A>();
    a = b //会报错
  • SuperA<G>A<G> extends SuperA<G>A<G>的实例可以赋给SuperA<G>类型的引用,因为A<G>中必有从SuperA<G>继承来的那部分。

    List<String> arraylist = new ArrayList<String>();

当声明一个变量/形参时,这个变量/形参的类型是一个泛型类或泛型接口,例如:Comparator<T>类型,但是无法确定<T>的具体类型,此时我们考虑使用类型通配符 ?

  • 通配符:?

    举例:ArrayList<?>

    G<?> 可以看作G<A>的父类,即可以将G<A>的对象赋值给G<?>类型的引用。

    • 读取数据:允许,读取的值的返回类型为 Object 类型。

    • 写入数据:除了 null 都不允许,因为参数类型是 ?,添加什么都不合适,编译器没用那么智能,不会帮你判断是哪种类型的ArrayList赋给 ArrayList<?> 的。

  • 有限制的通配符:

    List<? extends A>:可以将List<A>List<B>赋值给List<? extends A>,B 为 A 的子类,extends 可以理解为小于等于。

    • 读取数据:允许,读取的值的返回类型为 A 类型(多态)。

    • 写入数据:除了 null 外都不允许写入,因为会出现List<C>赋给List<? extends A>,但是想存入 B 的对象的情况,List<C>怎么可以存 B 呢?(C 继承 B 继承 A)

    List<? super A>:可以将List<A>List<B>赋值给List<? extends A>,B 为 A 的父类。super 可以理解为大于等于。

    • 读取数据:允许,读取的值的返回类型为 Object 类型(多态)。

    • 写入数据:可以写入 A 及其子类的对象,包括 null

因为泛型能存入指定类以及其子类,所以需要知道指定类的下边界,只有List<? super A>能做到所创的List一定大于等于 A,即下界为 A,所以能写入 A 及其子类的对象,?? extends A均无法做到,所以除了 null 都不能存。

上界是读的返回值类型。

下界及其子类是能写的内容。

利用 X 轴进行记忆。

上界下界
?Objectnull
? extends AAnull
? super AObjectA

5.多线程#

5.1 概述#

5.2 创建方式#

5.2.1 方式一:继承 Thread 类#

步骤:

  1. 创建一个继承了 Thread 类的子类。

  2. 重写 Thread 类的 run() 方法,将线程执行的操作写在 run() 方法中。

  3. 创建子类对象,并通过对象调用 start() 方法。作用:1、启动线程。2、调用线程的 run() 方法。

    一个对象只能 start 一次。想创建多个线程,则需要创建多个对象。

    public class MyThread extends Thread {
    //定义指定线程名称的构造方法
    public MyThread(String name) {
    //调用Thread的String参数的构造方法,指定线程的名称
    super(name);
    }
    /**
    * 重写run方法,完成该线程执行的逻辑
    */
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    System.out.println(getName()+":正在执行!"+i);
    }
    }
    }
    //在main()中使用
    //创建自定义线程对象1
    MyThread mt1 = new MyThread("子线程1");
    //开启子线程1:1、启动线程。2、调用线程的 run 方法。
    mt1.start();

5.2.2 方式二:实现 Runnable 接口#

步骤:

  1. 创建一个实现了 Runnable 接口的类。

  2. 实现接口中的 run() 方法,将线程执行的操作写在 run() 方法中。

  3. 创建实现类的对象,将此对象作为参数传到 Thread 类的构造器中,创建 Thread 类的实例。

    MyRunnable mr = new MyRunnable();
    //创建线程对象
    Thread t = new Thread(mr);
    t.start();
  4. Thread 类的实例(才是真的线程对象)调用 start() 方法。作用:1、启动线程。 2、调用线程的 run() 方法。

    若想创建多线程,则需要 new 多个 Thread 类的实例,但可以是实现类的同一个对象(比如mr),且共享这个实现类的属性(共享资源)。

    无论是方式一还是二,都是在主线程中调用start() 方法,然后再生成新的线程。

    Thread 类实际上也实现了Runnable接口。

5.2.3 方式三:实现 Callable 接口#

callable

5.2.4 方式四:线程池#

线程池

线程池在 JUC 中再学。

5.3 常用方法#

5.3.1 构造器#

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口中的 run() 方法
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字

5.3.2 方法#

Thread.currentThread():返回当前执行的线程对象的引用。

Thread.currentThread().getName:获取线程的名称。

对象.setName("线程1"):设置线程的名称。

Thread.currentThread().setName("线程1"):在子类或实现类的 run() 方法中调用此方法,可以给不是主线程的线程设置名称。

main 方法中调用,则是给主线程改名。

PrintNumber printNumber = new PrintNumber();
Thread thread = new Thread(printNumber,"线程名");
//这是通过Runnable接口实现的线程,而非方式一。
class PrintNumber extends Thread{
//可以使用构造器设置线程名,在子类构造器中使用super("线程名")调用Thread的带参构造器
public PrintNumber() {
super("123"); //这种命名方式只会影响方式一。
}
@Override
public void run() {
for(int i = 1;i <= 100;i++){
if(i%2==0){
System.out.println(Thread.currentThread().getName()+" " + i);
}
}
}
}

Thread.sleep(n):使调用此方法的线程休眠 n 毫秒。

是静态方法,在哪个线程中调用,哪个线程就休眠。因此在线程 A 中通过B.sleep(),休眠的也是线程 A;会抛出异常,需要使用异常处理。

Thread.yield():一旦执行此方法,就释放 CPU 的执行权。

对象.join():在线程 a 中,调用 b.join,则会阻塞线程 a,线程 b 执行完后,线程 a 才继续。

会抛出异常,需要使用异常处理。

对象.isAlive():判断线程是否存活,返回值为布尔类型。

优先级相关的方法

  • getPriority():获取线程的优先级。

    Thread.currentThread().getPriority()对象.getPriority()

  • setPriority():设置线程的优先级,范围为[1,10]。默认为 5。

    Thread 的三个属性:

    Thread.MAX_PRIORITY:10

    Thread.MIN_PRIORITY:1

    Thread.NORM_PRIORITY:5,默认优先级

5.4 同步#

5.4.1 Synchronized#

synchronized

在 Runnable 实现的线程里,同步监视器可以用 this 关键字。

继承 Thread 类实现的线程,同步监视器可以使用:继承子类的类名.class。

同步方法

public synchronized void show{ //默认同步监视器是 this
...
}
public static synchronized void show{ //默认同步监视器是:类名.class
...
}
//然后在 run() 中调用此方法。

5.4.2 Lock#

Lock 是一个接口,其中一个实现类为 ReentrantLock

public Windows extends Thread{
//创建Lock的实例,需要确保多个线程共用同一个Lock实例。
private static final ReentrantLock lock = new ReentrantLock();
public void run(){
try{
//上锁
lock.lock();
....需要上锁的代码
}
finally{
//释放
lock.unlock();
}
}
}

5.5 线程间通信#

生产者和消费者之间的关系。

wait和notify

这 3 个方法只能写在 Synchronized 的代码块或方法中,且调用者必须是同步监视器对象。

这 3 个方法声明在 Object 类中。

//使用两个线程打印1-100。线程1, 线程2交替打印
class Communication implements Runnable {
int i = 1;
public void run() {
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else
break;
try {
wait(); // 会抛出异常
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

sleep和wait的区别

6.File 类与 IO流#

6.1 File 类#

File 类及 IO 各种流,都定义在 java.io 包下。

一个 File 对象代表可能存在的一个文件或者文件目录。创建 File 对象时,即便不存在这个文件或文件目录也不会报错,只有在执行对文件的具体操作时才会报错。

File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身,即不能读写。如果需要访问文件内容本身,则需要使用 IO 流。

6.1.1 相对路径#

相对路径是看文件所在目录的关系,而没到文件本身。

比如:C:\new\a.htmlC:\new\temp\image.png,一个在 new 目录下,一个在 temp 目录下。

同一级目录(父节点相同)src="image.png"
下一级目录 src="images/image.png"
上一级目录 src="../image.png"
上上级目录 src="../../image.png" //java会把“..”当成目录名

但很遗憾, File 类无法通过这种方法找上级目录。

IDEA 中,单元测试中的相对路径,是相对当前的modulemain() 中的相对路径,是相对当前的project

D:\javacode\code_learning\code_learning\hello.txt
public static void main(String[] args) {
File file = new File("hello.txt");
System.out.println(file.getAbsoluteFile()); //得到文件的绝对路径
}
@Test
public void test(){
File file = new File("hello.txt");
System.out.println(file.getAbsoluteFile());
//D:\javacode\code_learning\code_learning\learn1\hello.txt
}

6.1.2 Java 中路径写法#

在 Windows 中,分隔符都是反斜杠,如:C:\new\a.html。但是 java 里反斜杠是转义字符,所以需要使用“\\”或“/”。

File file = new File("F:/hello.txt");
File file = new File("F:\\hello.txt");

6.1.3 构造器#

  • public File(String pathname) :以 pathname 为路径创建 File 对象,可以是绝对路径或者相对路径。

    //绝对路径
    File file = new File("F:/hello.txt");
    File file1 = new File("/hello.txt");
    //这也是种绝对路径,省略了project所在盘符。
    System.out.println(file1.getAbsolutePath()); // project在D盘,则输出D:\hello.txt
    //相对路径
    File file = new File("abc.txt");
  • public File(String parent, String child) :以 parent 为路径,child 为文件或目录创建File对象。

    C:\abc\hello.txt
    File file = new File("C:/abc","hello.txt");
    File file = new File("C:/abc","def");
    //C:\abc\def

    parent 一定是个目录的路径,child 可以是一个目录或文件。

  • public File(File parent, String child) :以 parent 为路径,child 为文件或目录创建File对象。

    C:\abc\def\hello.txt
    File file = new File("C:/abc","def");
    File file1 = new File(file,"hello.txt");

    parent 一定是个目录的路径,child 可以是一个目录或文件。与第二种的差别是,父路径以 File 对象给出。

6.1.4 常用方法#

1、获取文件和目录基本信息#
  • public String getName() :获取文件名/最后一级目录名。

    目录名是指:C:\abc\def中的 def。

  • public String getPath() :获取构造路径

  • public String getAbsolutePath():获取绝对路径

    当构造路径是绝对路径时,getPath()getAbsolutePath() 结果一样;当构造路径是相对路径时,结果才会不同。

    getPath() 获取的是构造器中传入的字符串,针对File(String parent, String child)public File(File parent, String child) ,获取的是父路径和子路径拼接后的字符串。

  • public File getAbsoluteFile():获取以绝对路径表示的文件

    返回值是一个 File 对象。sout时,只是调用了 File 中的 toString,打印的内容看起来和getAbsolutePath()一样而已。

  • public String getParent():获取上层文件目录路径。若无,返回null

    注意返回的是路径,而不是上一级目录。

    如:C:/abc/hello.txt返回的是C:\abc,而不是abc

    而对于相对路径,返回的也是相对路径。

    如:ac/abc/hello.txt,返回值为ac\abc

  • public long length() :获取文件长度(即:字节数)

    不能获取目录的长度。

    文件实际的大小需要打开属性看,而不是直接通过“大小”看。

    文件大小

  • public long lastModified() :获取最后一次的修改时间,毫秒值

    返回的是时间戳。

    如果 File 对象代表的文件或目录存在,则 File对象实例初始化时,就会用硬盘中对应文件或目录的属性信息(例如,时间、类型等)为File对象的属性赋值,否则除了路径和名称,File对象的其他属性将会保留对应数据类型的默认值。

    image-20220412215446368
2、列出目录的下一级#

操作对象必须是目录,若是文件则返回 null

  • public String[] list() :返回一个 String 数组,表示该 File 目录中的所有子文件或目录。

    增强 for 循环遍历打印时,String 对象会自动调用 toString(),打印的就是此目录下的文件名或子目录名。

    File dir = new File("D:\\论文");
    String[] subs = dir.list();
    for (String sub : subs) {
    System.out.println(sub);
    }
    /*
    如何写.png
    车联网
    车联网.md
    车联网md图片
    */
  • public File[] listFiles() :返回一个 File 数组,表示该 File 目录中的所有的子文件或目录。

    增强 for 循环遍历打印时,File 对象会自动调用 toString(),打印的是 getPath()的结果。

4、判断功能的方法#
  • public boolean exists() :此 File 表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此 File 表示的是否为目录。
  • public boolean isFile() :此 File 表示的是否为文件。
  • public boolean canRead() :判断是否可读。
  • public boolean canWrite() :判断是否可写。
  • public boolean isHidden() :判断是否隐藏。
5、创建、删除功能#
  • public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false

  • public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。

  • public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建。

  • public boolean delete() :删除文件或者文件夹。

    删除注意事项:① Java中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。

    //文件的创建
    File f = new File("aaa.txt");
    System.out.println("aaa.txt是否存在:"+f.exists());
    System.out.println("aaa.txt是否创建:"+f.createNewFile());
    System.out.println("aaa.txt是否存在:"+f.exists());
    // 创建一级目录
    File f3= new File("newDira\\newDirb");
    System.out.println("newDira\\newDirb创建:" + f3.mkdir());
    File f4= new File("newDir\\newDirb");
    System.out.println("newDir\\newDirb创建:" + f4.mkdir());
    // 创建多级目录
    File f5= new File("newDira\\newDirb");
    System.out.println("newDira\\newDirb创建:" + f5.mkdirs());
    // 文件的删除
    System.out.println("aaa.txt删除:" + f.delete());

6.2 IO 流#

Java 程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行,可以看做是一种数据的流动。

image-20220503123117300

6.2.1 流的分类#

  • 按数据的流向不同分为:输入流输出流。(占在内存的角度)

    • 输入流 :把数据从磁盘上读取到内存中的流。
    • 输出流 :把数据从内存中写出到磁盘上的流。
  • 按操作数据单位的不同分为:字节流字符流字节流是直接与数据产生交互,而字符流在与数据交互之前要经过一个缓冲区

    • 字节流 :以字节为单位,读写数据的流。以 InputStreamOutputStream 结尾。
    • 字符流 :以字符为单位,读写数据的流。以 ReaderWriter 结尾。

    因为有些字符不止一个字节。

  • 根据IO流的角色不同分为:节点流处理流

    • 节点流:直接从数据源或目的地读写数据。

      image-20220412230745170

    • 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上。

      image-20220412230751461

    处理流相当于套娃,如上图中的第二大和最大的圆柱。

6.2.2 流的 API#

  • 四个抽象的(abstract)基类
(抽象基类)输入流输出流
字节流InputStreamOutputStream
字符流ReaderWriter

Java 提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。不能操作图片,视频等非文本文件。

常见的文本文件有如下的格式:.txt、.java、.c、.cpp、.py等。

注意:.doc、.xls、.ppt这些都不是文本文件。

6.2.3 节点流(也叫文件流)#

1、FileReader(其父类为 InputStreamReader)#

构造器

  • FileReader(File file):通过File类创建一个新的 FileReader ,给定要读取的 File 对象。使用系统默认的字符编码 UTF-8

  • FileReader(String filename): 通过字符串创建一个新的 FileReader ,给定要读取的 File 对象。使用系统默认的字符编码 UTF-8

    filename 传入上述构造器后,也有new File(filename);这个操作。

    FileReader fr = new FileReader("E:\\hello.txt"); 相当于

    FileReader fr = new FileReader(new File("E:\\hello.txt"));

//使用try-catch-finally处理异常。保证流是可以关闭的
@Test
public void test2() {
FileReader fr = null;
try {
//1. 创建File类的对象,对应着物理磁盘上的某个文件
File file = new File("hello.txt");
//2. 创建FileReader流对象,将File类的对象作为参数传递到FileReader的构造器中
fr = new FileReader(file);
//3. 通过相关流的方法,读取文件中的数据
/*
read():每次从对接的文件中读取一个字符。并将此字符返回。
如果返回值为-1,则表示文件到了末尾,可以不再读取。
*/
int data;
while ((data = fr.read()) != -1) {
System.out.println((char)data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭相关的流资源,避免出现内存泄漏
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}

Reader 抽象类提供了读取字符流的方法:

  • public int read(): 从输入流读取一个字符。 虽然读取了一个字符,但是会自动提升为int类型,返回该字符的 Unicode 编码值(强转为char类型就可以显示字符)。如果已经到达流末尾了,则返回 -1。

  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组cbuf中 。每次最多读取 cbuf.length 个字符。返回值len为实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回 -1。

    //错误:遍历了整个数组,可能读到上次的脏数据。
    for(int i = 0;i < cbuf.length;i++){
    System.out.print(cbuf[i]);
    }
    //正确:每次遍历实际读到的个数。
    for(int i = 0;i < len;i++){
    System.out.print(cbuf[i]);
    }
  • public int read(char[] cbuf,int off,int len):从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中,从 cbuf[off] 开始的位置存储。每次最多读取 len 个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回 -1。

  • public void close() :关闭此流并释放与此流相关联的任何系统资源。

    注意:当完成流的操作时,必须调用 close() 方法,释放系统资源,否则会造成内存泄漏。

2、FileWriter(其父类为OutputStreamWriter)#

使用系统默认的字符编码 UTF-8。

  • FileWriter(File file): 创建一个 FileWriter,参数为要读取文件的 File 对象。
  • FileWriter(String fileName): 创建一个 FileWriter,参数为要读取文件的名称。
  • FileWriter(File file,boolean append): 创建一个 FileWriter,指明是否在现有文件末尾追加内容,当 append 参数为 true 时,追加。

Writer 抽象类提供了输出字符流的方法:

  • public void write(int c) :写出单个字符。
FileWriter fw = new FileWriter("fw.txt");
// 写出数据
fw.write(97); // 写出第1个字符
fw.write('q'); // 写出第2个字符,这里用到了自动类型提升
  • public void write(char[] cbuf) :写出字符数组。

  • public void write(char[] cbuf, int off, int len) :写出字符数组的某一部分。off:数组的开始索引;len:写出的字符个数。

  • public void write(String str) :写出字符串。

  • public void write(String str, int off, int len) :写出字符串的某一部分。off:字符串的开始索引;len:写出的字符个数。

  • public void flush() :刷新该流的缓冲。

  • public void close() :关闭此流。

  • 总结:

    FileReader

3、FileInputStream 与 FileOutputStream#

除了是字节流外,操作和 FileReaderFileWriter 差不多。

FileInputStream

FileInputStream 构造方法:

  • FileInputStream(File file)

  • FileInputStream(String name)

    FileReader 的构造方法一 一对应。

InputStream 抽象类的方法:

  • public int read(): 从输入流读取一个字节。返回读取的字节值。虽然读取了一个字 节,但是会自动提升为 int 类型。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 >b 中 。每次最多读取 b.length 个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(byte[] b,int off,int len):从输入流中读取一些字节数,并将 它们存储到字节数组 b 中,从 b[off] 开始存储,每次最多读取 len 个字节 。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public void close() :关闭此输入流,并释放与此流相关联的任何系统资源。

FileOutputStream 构造方法:

  • FileOutputStream(File file)

  • FileOutputStream(String name)

  • public FileOutputStream(File file, boolean append)

    FileWriter 的构造方法一 一对应。

OutputStream 抽象类的方法:

  • public void write(int b) :将指定的字节输出流。虽然参数为int类型四个字节,但实际上只会写入一个字节的信息。
  • public void write(byte[] b):将 b.length个字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。
  • public void flush() :刷新此输出流,并强制任何缓冲的输出字节被写出。
  • public void close() :关闭此输出流,并释放与此流相关联的任何系统资源。

注意:

  • char 数组改成了 byte 数组。
  • 字符流只能操作文本文件。
  • 字节流通常是用来处理非文本文件的。但是,如果涉及到文本文件的复制操作,也可以使用字节流。

6.2.4 处理流#

1、缓冲流#

缓冲流的基本原理:在创建流对象时,内部会在内存创建一个缓冲区数组(8KB),通过缓冲读写,减少系统IO次数,从而提高读写的效率。

读文件和写文件都使用了缓冲区。

缓冲区数组指的是缓冲流内部的数组,不是由我们创建的准备作为write()参数的数组。

缓冲区数组

缓存流的作用:提高文件读写的效率,不用频繁地与磁盘交互。

缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

  • 字节缓冲流

    • BufferedInputStream使用的方法:read(byte[] buffer)
    • BufferedOutputStream 使用的方法:write(byte[] buffer,0,len)
  • 字符缓冲流

    • 读写方法和 Reader 相同。

    • BufferedReader特有的方法:public String readLine()

    • BufferedWriter特有的方法:public void newLine(),输出一个换行符到文件,这个方法相当于调用 bw.write("\n");

      readLine():每次读取一行的文本数据,返回的字符串是不包括换行符的,返回值为 String,没读到内容则返回 null。当写入时,需在返回值后拼接一个'\n',或者调用newLine() 方法。

      @Test
      public void testReadLine()throws IOException {
      // 创建流对象
      BufferedReader br = new BufferedReader(new FileReader("in.txt"));
      // 定义字符串,保存读取的一行文字
      String line;
      // 循环读取,读取到最后返回null
      while ((line = br.readLine())!=null) {
      System.out.print(line + "\n");
      }
      // 释放资源
      br.close();
      }

      read(char[] cBuffer):读到的包括换行符。

实现步骤:

缓冲流实现步骤

关闭资源时,先关缓冲流,再关文件流。

关外层的流会自动把内层流关闭,所以可以省略内层流的关闭。

flush():立即将缓冲区的内容写入。(不是所有的类都有 flush() 方法,只有实现了缓冲或数据刷新的类才具备这一方法。XXXWriter一般才使用,BufferedOutputStream 也能用)

  • flush() :刷新缓冲区,流对象可以继续使用。
  • close() :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

注意:即便是flush()方法写出了数据,操作的最后还是要调用close()方法,释放系统资源。

2、转换流#

转换流的使用场景是文件字符集与默认字符集不同的时候。读取指定的编码格式的文件时,特别是不是默认的格式时,就需要转换流。

作用:转换流是字节与字符间的桥梁!

image-20220412231533768

InputStreamReader:输入型字节流转输入型字符流,是 Reader 的子类。

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。

  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流,以指定的字符集来解释这个字节流

    文件都是二进制形式存储,所以先以字节流读入,再用字符流以指定的字符集来解释这个字节流。

//使用默认字符集,默认UTF-8
InputStreamReader isr1 = new InputStreamReader(new FileInputStream("in.txt"));
//使用指定字符集
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");

OutputStreamWriter:输出型字符流转输出型字节流,是Writer的子类。

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。

  • OutputStreamWriter(OutputStream in,String charsetName): 创建一个指定字符集的字符流,以指定字符集写进对应的二进制编码

    因为我们写的时候是用字符,但存入文件中的实际是二进制数据,所以需要字符转字节。

//使用默认字符集
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
//使用指定的字符集
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") ,"GBK");

字符是中介,字符都是那个字符,但编码格式不同导致了字节不同。

但值得注意的是,缓冲流构造器里传入的 IO 流都是字节流

3、对象流#
  • 对象流:ObjectOutputStream、ObjectInputStream
    • ObjectOutputStream:将 Java ==基本数据类型和对象==写入字节输出流中,可以实现 Java 各种基本数据类型的数据以及对象的持久存储。
    • ObjectInputStream:对之前使用 ObjectOutputStream 写出的==基本数据类型的数据和对象==进行读入操作,保存在内存中。

说明:对象流的强大之处就是可以把 Java 中的对象写入到文件中,也能把对象从文件中还原回来。

ObjectOutputStream中的构造器:

public ObjectOutputStream(OutputStream out) : 创建一个指定的 ObjectOutputStream。

FileOutputStream fos = new FileOutputStream("game.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);

ObjectInputStream中的构造器:

public ObjectInputStream(InputStream in) : 创建一个指定的 ObjectInputStream。

FileInputStream fis = new FileInputStream("game.dat");
ObjectInputStream ois = new ObjectInputStream(fis);

对象序列化机制允许把内存中的 Java 对象转换成与平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java 对象。

image-20220503123328452

实现原理:

  • 序列化:用ObjectOutputStream类保存基本类型数据或对象的机制。方法为:

    • public final void writeObject (Object obj) : 将指定的对象写出。
  • 反序列化:用ObjectInputStream类读取基本类型数据或对象的机制。方法为:

    • public final Object readObject () : 读取一个对象。

自定义序列化的类

如果有多个对象需要序列化,则可以将对象放到集合中,再序列化集合对象即可。

transient 修饰的属性的值会采用默认值,比如 null0

序列化针对的是对象,所以类变量不能被序列化。 static 修饰的属性会采用 JVM 存储的值,若序列化和反序列化写在一个进程(单元测试类),JVM 存储的值和对象的值相同;若写在两个进程中,JVM 重新加载类变量,JVM 存储的值将会变成类变量的初始值。(初始值不等同默认值,初始化有几种:默认初始化,显式初始化、代码块初始化等)

6.2.5 标准输入、输出流#

System.in :标准输入流,默认从键盘输入。

System.out:标准输出流,默认从显示器输出。(理解为控制台输出)

System.inInputStream 类型的对象。

System.outPrintStream 类型的对象。

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

通过调用如下的方法,可以修改输入流、输出流的位置。

  • setIn(InputStream is)
  • setOut(PrintStream ps)

6.2.6 打印流#

打印流提供了非常方便的打印功能,可以打印==任何类型==的数据信息,例如:小数,整数,字符串。

打印流:PrintStreamPrintWriter,只有输出流。

打印流也是处理流,底层会用到 FileOutputStreamFileWriter

提供了一系列重载的 print()println() 方法,用于多种数据类型的输出。write() 方法的参数通常为字节数组、字符数组或字符串,不利于输出,实际上 print() 底层调用了 write() 方法。

public void print(char c) {
write(String.valueOf(c));
}

image-20220131021502089

image-20220131021528397

7.网络编程#

本地回路地址:127.0.0.1,代表本机。

端口号:可以唯一标识主机中的进程(应用程序)。

不同的进程分配不同的端口号。

8.反射#

反射1

反射2

8.1 Class类#

Class类

类加载完之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。

要想解剖一个类,必须先要获取到该类的 Class 对象。所以 Class 对象是反射的根源。

特点:

  • Class 本身也是一个类
  • Class 对象只能由系统建立对象
  • 一个加载的类在 JVM 中只会有一个 Class 实例
  • 一个 Class 对象对应的是一个加载到 JVM 中的一个 .class 文件
image-20220514180100176

说明:上图中字符串常量池在 JDK6 中存储在方法区;JDK7 及以后,存储在堆空间。

8.1.1 获取 Class 实例的方式#

  • 运行时类型的静态属性:class

    Class clazz1 = 类名.class;

  • 通过某个类的对象调用 getClass()

    User u1 = new User();
    Class clazz2 = u1.getClass;
  • 调用 Class 的静态方法 forName(String className)

    String classname = "包名.类名"; //全类名 = 包名.类名
    Class clazz3 = Class.forName(className);

    第三种能更好地体现动态性,因为类名是参数,没有写死。

8.1.2 哪些类型可以有Class对象#

简言之,所有Java类型!

(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类 (2)interface:接口 (3)[]:数组 (4)enum:枚举 (5)annotation:注解@interface (6)primitive type:基本数据类型 (7)void

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;
Class c9 = 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

8.2 ClassLoader#

8.2.1 类加载器的作用#

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class 对象,作为方法区中类数据的访问入口。

8.2.2 类加载器分类#

有一系列类加载器,其中 SystemClassLoader 是默认用于加载我们自定义类的加载器。Java 的核心 API 的加载器是引导类加载器(Bootstrap ClassLoader)。

8.2.3 使用 ClassLoader 获取流#

类加载器的一个主要方法:getResourceAsStream(String str) 获取存放.class文件的目录下的指定文件的输入流。

//需要掌握如下的代码
@Test
public void test5() throws IOException {
Properties pros = new Properties();
//方式1:此时默认的相对路径是当前的module
// FileInputStream is = new FileInputStream("info.properties");
// FileInputStream is = new FileInputStream("src//info1.properties");
//方式2:使用类的加载器
//此时默认的相对路径是存放.class文件的目录
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");
pros.load(is);
//获取配置文件中的信息
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name = " + name + ", password = " + password);
}

获取流的方法也可以写成:类名.class.getClassLoader().getResourceAsStream("resources下的文件");。类名是指此代码所在类的名字如果上述代码写在TestJson中,则是TestJson.class.getClassLoader().getResourceAsStream("resources下的文件");,此段代码是告诉java在TestJson.class所在目录下找文件。

resources目录需要被标记为Resources Root,即资源文件夹。在编译过后,resources里的文件和.class文件存放在同一目录。

同级目录

目录

8.3 反射的基本应用#

==前提是获得 Class 实例,再调用以下的方法。==

8.3.1 创建运行时对象#

方式1:

通过 Class 对象调用 newInstance()。[ jdk9及以后过时了]

需要满足条件:

  • 运行时类必须有空参构造器,否则会报 InstantiationException
  • 空参构造器的权限要合适,必须有权限调用此构造器,没有权限访问会报 IllegalAccessException
Class<Person> clazz = Person.class;
Person per = clazz.newInstance(); //调用空参构造器

方式2:

通过获取构造器对象来进行实例化。

步骤:

1)通过 Class 类的 getDeclaredConstructor(Class … parameterTypes) 取得本类的指定形参类型的构造器

2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。

3)通过 Constructor 实例化对象。

如果构造器的权限修饰符修饰的范围不可见,也可以调用 setAccessible(true)

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//(1)获取Class对象
Class<Person> clazz = (Class<Person>) Class.forName("com.lc.file1.Person");
/*
* 获取Person类的有参构造
* 如果构造器有多个,我们通常是根据形参【类型】列表来获取指定的构造器
* 例如:public Person(int age, String name)
*/
//(2)获取构造器对象
Constructor<Person> constructor = clazz.getDeclaredConstructor(int.class,String.class);
//(3)创建实例对象
// T newInstance(Object... initargs) 这个Object...是在创建对象时,给有参构造的实参列表
Person p = constructor.newInstance(12,"lc");
System.out.println(p);
}

8.3.2 获取运行时类的所有属性和方法#

属性:

public Field[] getFields() :获取运行时类及其父类中 public 的属性。

public Field[] getDeclaredFields() :获取运行时类中声明(任意权限)的属性。

Class<Person> clazz = (Class<Person>) Class.forName("com.lc.file1.Person");
Field[] fields = clazz.getFields();
for (Field f : fields ) {
System.out.println(f);
}

反射还可以获取修饰符、变量类型、变量名。

方法:

public Method[] getMethods():获取运行时类及其父类中 public 的方法。

public Method[] getDeclaredMethods():获取运行时类中声明(任意权限)的方法。

8.3.3 获取运行时类的父类、接口、包、泛型#

父类:public Class<? Super T> getSuperclass()

即便是带泛型的父类,用getSuperclass()得到只有父类名。想要得到父类<泛型>,需要用:Type getGenericSuperclass()

获取父类的泛型:

Type type = clazz.getGenericSuperclass(); //Type 是一个接口,Class实现了这个接口
// 如果父类带泛型,则可以强转为ParameterizedType
ParameterizedType pt = (ParameterizedType) type;
// 获取泛型父类的泛型实参,结果是数组,因为可能有多个泛型参数。
Type[] typeArray = pt.getActualTypeArguments();
for (Type type2 : typeArray) {
System.out.println(((Class)type2).getName()); //java.lang.String,若不调用 getName(),则返回 class java.lang.String。
}

接口:public Class<?>[] getInterfaces()

包:Package getPackage()

8.3.4 调用指定的属性、方法#

调用指定的属性:

在反射机制中,可以直接通过 Field 类操作类中的任何权限的属性,通过 Field 类提供的 set()get() 方法就可以完成设置和取得属性内容的操作。

步骤:

(1)获取该类型的 Class 对象

Class clazz = Class.forName("包.类名");

(2)获取属性对象

Field field = clazz.getDeclaredField("属性名");

(3)如果属性的权限修饰符不是 public,那么需要设置属性可访问

field.setAccessible(true);

(4)设置指定对象 obj 上此 Field 的属性内容

field.set(obj,"属性值");

如果 field 静态变量,那么实例对象可以省略,用 null 表示。

(5)取得指定对象obj上此 Field 的属性内容

Object value = field.get(obj);

如果 field 是静态变量,那么实例对象可以省略,用 null 表示。

调用指定的方法

(1)获取该类型的 Class 对象

Class clazz = Class.forName("包.类名");

(2)获取方法对象

Method method = clazz.getDeclaredMethod("方法名",方法的形参类型列表);

比如Method method = clazz.getDeclaredMethod("add",int.class,int.class);

(3)创建实例对象

Object obj = clazz.newInstance();

(4)调用方法

Object result = method.invoke(obj, 方法的实参值列表);

如果方法的权限修饰符修饰的范围不可见,也可以调用 method.setAccessible(true);

如果方法是静态方法,实例对象也可以省略,用 null 代替。

invoke()返回值为 Object 类型,如果 method 对应方法的返回值为 void,则 invoke()的返回值为 null

9.常见的 API#

9.1 比较器#

9.1.1Comparable 接口(自然排序)#

如果自定义类(引用类型)想实现排序,这个类则需要重写 Comparable 接口的 compareTo(Object obj) 方法。

两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。

  • 返回值为,当前对象 this 大于形参对象 obj

  • 返回值为,当前对象 this 小于形参对象 obj

  • 返回值为 0 ,当前对象 this 和形参对象 obj 一样大

实现 Comparable 接口的对象列表(和数组)可以通过 Collections.sortArrays.sort 进行自动排序。

默认升序,但是只要在返回值前面加个负号就能实现降序。

class Person implements Serializable,Comparable<Person>{
//Comparable后可以加泛型,指定比较的类
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name);
}
@Override
public int compareTo(Person o) {
// 先按年龄升序排序
int ageComparison = this.age - o.age;
if (ageComparison != 0) {
return ageComparison;
}
// 如果年龄相等,则按姓名A-Z升序排序
return this.name.compareTo(o.name);
//调用了 String 里的 compareTo()
}

Comparable 的典型实现:(默认都是从小到大排列)

  • String:按照字符串中字符的 Unicode 值进行比较。
  • Character:按照字符的 Unicode 值来进行比较。
  • 数值类型对应的包装类以及 BigInteger、BigDecimal:按照它们对应的数值大小进行比较。
  • Boolean:true 对应的包装类实例大于 false 对应的包装类实例。
  • Date、Time等:后面的日期时间比前面的日期时间大。

9.1.2 Comparator 接口(定制排序)#

步骤:

  1. 创建一个实现了 Comparator 接口的类 A,重写compare(obj o1,obj o2),在此方法中指明如何比较类 B 的大小。
  2. B[] b = new B[]{...}; Arrays.Sort(b,A);

B 无需实现 Comparable 接口(无需修改 B 的代码),借助 A 实现 Comparator 接口,也可以实现比较。

两个对象即通过 compare(Object o1,Object O2) 方法的返回值来比较大小。

  • 返回值为正,o1 大。
  • 返回值为负,o1 小。
  • 返回值为 0 ,一样大。
class A implements Comparator<Person>{
@Override
public int compare(Person o1, Person o2) {
// 先按年龄升序排序
int ageComparison = o1.age - o2.age;
if (ageComparison != 0) {
return ageComparison;
}
// 如果年龄相等,则按姓名字母顺序排序
return o1.name.compareTo(o2.name);
}
}
...
//在main()中,创建定制排序类的实例
A a = new A();
Arrays.sort(p,a); //传入定制排序类的实例

9.2 时间 API#

9.2.1 LocalXxx#

LocalDate:获取当地的日期。

LocalTime:获取当地的时间。

LocalDateTime:获取当地的日期和时间。

实例化的方法:

三者方法相同,以 LocalDate 为例,实例化有静态方法LocalDate.now()和LocalDate.of(年,月,日)

LocalDate now = LocalDate.now(); //获取现在的日期的对象
LocalDate date = LocalDate.of(2019,9,3); //获取指定时间的对象
System.out.println(now); //2023-11-03
System.out.println(date);//2019-09-03

常用方法:

//getXxx():得到日、月、年的某一个。
//withXxx():修改日、月、年。
//plucsXxx():向当前对象添加几天、几周、几个月、几年、几小时。
//minusXxx():从当前对象减去几月、几周、几天、几年、几小时。

不可变性,凡是要修改值的,都是返回一个新对象。

9.2.2 Instant#

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

方法描述
now()静态方法,返回默认UTC时区的Instant类的对象
ofEpochMilli(long epochMilli)静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
atOffset(ZoneOffset offset)结合即时的偏移来创建一个 OffsetDateTime
toEpochMilli()返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳
Instant instant = Instant.now();
System.out.println(instant); //2023-11-17T10:42:21.860487100Z

9.2.3 日期时间格式化:DateTimeFormatter#

用来格式化 LocalDate、LocalTime、LocalDateTime 的值。

ofPattern(String pattern) :静态方法,返回一个指定字符串格式的DateTimeFormatter。

//自定义格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
LocalDateTime now = LocalDateTime.now();
//格式化:日期、时间 --->字符串
String format = dateTimeFormatter.format(now);
System.out.println(format); //2023-11-03 10:19:49

parse(CharSequence text) 将指定格式的字符序列解析为一个日期、时间。

TemporalAccessor accessor = dateTimeFormatter.parse("2022/12/04 21:05:42");
LocalDateTime localDateTime = LocalDateTime.from(accessor);
//此方法接受参数temporal,该参数指定要转换为LocalDateTime实例的时间对象。
System.out.println(localDateTime); //2022-12-04T21:05:42

这篇文章是否对你有帮助?

Java学习笔记
作者
蜀枕清何
发布于
2024-02-28
许可协议
CC BY-NC-SA 4.0