Java高级学习笔记

学习来源:尚硅谷

学习时间:2022年3月23日

1 多线程

略,详见实用技术-Java并发编程学习笔记

2 常用类

2.1 字符串相关的类

2.1.1 概述

  • String类:代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。

    • String是一个final类,代表不可变的字符序列
    • 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。
    • String实现了Serializable接口,表示字符串是支持序列化的;实现了Comparable接口,表示字符串可以比较大小
    • String对象的字符内容是存储在一个字符数组value[]中的。
    • String类的部分源码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

      @Stable
      private final char[] value; // final修饰,代表不可变

      private int hash;

      // ...
      }

2.1.2 String的不可变性

通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

字符串常量池中不会存储相同内容的字符串。

  • 不可变性(保护性拷贝)
    • 当对字符串重新赋值时,需要新开辟内存区域进行赋值,不能改变原有的内存区域存储的值
    • 当对现有的字符串进行连接操作时,也需要新开辟内存区域进行赋值
    • 当调用String的方法修改指定的字符或字符串时,也需要新开辟内存区域进行赋值

代码示例和图演示

1
2
String s1 = "abc"; // 字面量定义字符串
String s2 = "abc";

image-20220323150736689

1
2
3
String s1 = "abc";
String s2 = "abc";
s1 = "hello";

image-20220323151016238

1
2
3
4
5
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s1); // abc
System.out.println(s2); // abcdef

image-20220323151523400

1
2
3
4
String s1 = "abc";
String s2 = s1.replace("a", "m");
System.out.println(s1); // abc
System.out.println(s2); // mbc

image-20220323151800606

2.1.3 String对象的创建

① 概述和使用

可以通过字面量或者new + 构造器的方式来创建String对象:

1
2
3
4
5
6
7
8
9
10
11
12
String str = "hello";

//本质上this.value = new char[0];
String s1 = new String();

//this.value = original.value;
String s2 = new String(String original);

//this.value = Arrays.copyOf(value, value.length);
String s3 = new String(char[] a);

String s4 = new String(char[] a,int startIndex,int count);
  • 字面量和new的区别

    • 字符串常量存储在字符串常量池,目的是共享

    • 字符串非常量对象存储在中。

代码示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void test2() {
// 此时s1和s2地址对应的数据都在字符串常量池中
String s1 = "abc";
String s2 = "abc";

// 此时s3和s4地址对应的数据都在堆空间中
String s3 = new String("abc");
String s4 = new String("abc");
// 注意,==比较的是变量的地址值
// equals比较的是内容
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s3 == s4); // false

System.out.println(s1.equals(s3)); // true
System.out.println(s3.equals(s4)); // true
}

image-20220323153148874

代码示例2

1
2
3
Person p1 = new Person("Tom", 12);
Person p2 = new Person("Tom", 12);
System.out.println(p1.name == p2.name); // true

image-20220323154107647

1
2
3
4
Person p1 = new Person("Tom", 12);
Person p2 = new Person("Tom", 12);
p1.name = "Jack";
System.out.println(p2.name); // Tom

image-20220323154225800

问答题

1
String s = new String("abc");

问:在内存中创建了几个对象?

答:两个,一个是堆空间中的String对象,一个是String对象中char[]属性对应的常量池中的数据。

② 不同拼接操作的对比
  • 常量与常量的拼接结果在常量池,且常量池中不会存在相同内容的常量
  • 只要拼接内容中有一个是变量,结果就在中,相当于new了一个字符串
  • 如果拼接的结果调用intern()方法,返回值就在常量池中

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test0() {
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;

System.out.println(s3 == s4); // true
System.out.println(s3 == s5); // false
System.out.println(s3 == s6); // false
System.out.println(s3 == s7); // false
System.out.println(s5 == s6); // false
System.out.println(s5 == s7); // false
System.out.println(s6 == s7); // false

String s8 = s5.intern();
System.out.println(s3 == s8); // true
}

图示:

image-20220325171940461

1
2
3
4
5
6
7
@Test
public void test1() {
String s1 = "javaEEhadoop";
final String s2 = "javaEE"; // s2是常量
String s3 = s2 + "hadoop";
System.out.println(s1 == s3); //true
}

注意:由final修饰的是常量,即s2是常量,不是变量。因此第五句代码本质上是常量与常量的拼接。

面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StringTest {
String str = new String("good");
char[] ch = { 't', 'e', 's', 't' };

public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'b';
}

public static void main(String[] args) {
StringTest ex = new StringTest();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.println(ex.ch);
// 打印结果:
// good and best
}

}

说明:关于值传递:

  • 基本数据类型传递的是数据本身
  • 引用数据类型传递的是地址值

2.1.4 常用方法

  • int length():返回字符串的长度
  • char charAt(int index):返回某索引处的字符
  • isEmpty():判断是否是空字符串
  • String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
  • String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
  • String trim():返回字符串的副本,忽略前导空白和尾部空白
  • boolean equals(Object obj):比较字符串的内容是否相同
  • boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
  • String concat(String str):将指定字符串连接到此字符串的结尾,等价于用“+”
  • int compareTo(String anotherString):比较两个字符串的大小,返回差值
  • String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
  • String substring(int beginIndex, int endIndex):返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串(左闭右开)。
  • boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
  • boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
  • boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
  • boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
  • int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引,找不到返回-1
  • int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
  • int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
  • int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
  • 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个,如果超过了,剩下的全部都放到最后一个元素中

代码示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test1() {
String s1 = "HelloWorld";
System.out.println(s1.length()); // 10
System.out.println(s1.charAt(1)); // e
System.out.println(s1.isEmpty()); // false

String s2 = s1.toUpperCase(Locale.ROOT);
System.out.println(s1); // HelloWorld
System.out.println(s2); // HELLOWORLD

String s3 = " hello world ";
String s4 = s3.trim();
System.out.println(s3); // 不变
System.out.println(s4); // hello world
}

代码示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test2() {
String s1 = "abc";
String s2 = "def";
String s3 = s1.concat(s2);
System.out.println(s3); // abcdef

String s4 = "abc";
String s5 = new String("abz");
String s6 = "abc";
System.out.println(s4.compareTo(s5)); // -23
System.out.println(s4.compareTo(s6)); // 0

String s7 = "Hongyi";
String s8 = s7.substring(1);
String s9 = s7.substring(1,3);
System.out.println(s8); // ongyi
System.out.println(s9); // on
}

代码示例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test3() {
String s1 = "helloworld";
boolean b1 = s1.endsWith("rld");
boolean b2 = s1.startsWith("ll");
System.out.println(b1); // true
System.out.println(b2); // false

boolean b3 = s1.contains("llo");
System.out.println(b3); // true

int i1 = s1.indexOf("lo");
int i2 = s1.indexOf("m");
System.out.println(i1); // 3
System.out.println(i2); // -1
}

代码示例4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test4() {
String s1 = "helloworld";
String s2 = s1.replace("h", "a");
System.out.println(s1); // helloworld
System.out.println(s2); // aelloworld

String s3 = s1.replace("hello", "hongyi");
System.out.println(s3); // hongyiworld

String s4 = "12hello34world4";
// 把字符串中的数字替换成逗号 , 如果结果中开头和结尾有 , 的话则去掉
String s5 = s4.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
System.out.println(s5); // hello,world
}

代码示例5

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test5() {
String s1 = "hello|world|java";
String[] s2 = s1.split("\\|");
for (String s : s2) {
System.out.println(s);
// hello
// world
// java
}
}

2.1.5 数据转换

① 基本数据类型
  • 字符串 —> 基本数据类型、包装类
    • Integer包装类的public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
    • 类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test0() {
String s1 = "123";
int num = Integer.parseInt(s1);
System.out.println(num);

String s2 = "true";
boolean flag = Boolean.parseBoolean(s2);
System.out.println(s2);

String s3 = "123.123";
double num1 = Double.parseDouble(s3);
System.out.println(num1);
}
  • 基本数据类型、包装类 —> 字符串
    • 调用String类的public String valueOf(int n)可将int型转换为字符串
    • 相应的重载方法:valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换
1
2
3
4
5
6
@Test
public void test1() {
int num = 123;
String s = String.valueOf(num);
System.out.println(s);
}
② char数组
  • 字符数组 —> 字符串
    • String 类的构造器:String(char[]) 和 String(char[], int offset, int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。
1
2
3
4
5
6
7
@Test
public void test3() {
char[] arr = new char[]{'h', 'e', 'l', 'l', 'o'};
// 利用构造器创建字符串
String s = new String(arr);
System.out.println(s);
}
  • 字符串 —>字符数组
    • public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
    • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。
1
2
3
4
5
6
7
8
@Test
public void test2() {
String s1 = "123abc";
char[] charArray = s1.toCharArray();
for (char c : charArray){
System.out.println(c);
}
}
③ byte数组
  • 字节数组 —> 字符串
    • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
    • String(byte[], int offset, int length) :用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
1
2
3
4
5
6
7
8
@Test
public void test5() {
String s1 = "abc123";
byte[] bytes = s1.getBytes();
// 采用默认字符集解码
String s = new String(bytes);
System.out.println(s); // abc123
}
  • 字符串 —> 字节数组
    • public byte[] getBytes() :使用平台的默认字符集将此 String 编码为byte 序列,并将结果存储到一个新的 byte 数组中。
    • public byte[] getBytes(String charsetName) :使用指定的字符集将 此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。
1
2
3
4
5
6
7
8
9
@Test
public void test4() {
String s1 = "abc123";
// 采用默认字符集编码
byte[] bytes = s1.getBytes();
for(byte b : bytes) {
System.out.println(b); // 97,98,99,49,50,51
}
}

2.1.6 StringBuffer和StringBuilder类

① 介绍

java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象。很多方法与String相同。作为参数传递时,方法内部可以改变值。

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

  • String、StringBuffer、StringBuilder三者异同

    • String(JDK1.0):不可变字符序列,底层使用char[]存储字符
    • StringBuffer(JDK1.5):可变字符序列、效率低、线程安全,底层使用char[]存储字符

      1
      2
      3
      4
      5
      6
      7
      @Test
      public void test0() {
      StringBuffer sb1 = new StringBuffer("abc");
      // 该方法没有返回值,修改的就是sb1本身的值
      sb1.setCharAt(0, 'm');
      System.out.println(sb1); // mbc
      }
    • StringBuilder(JDK5.0):可变字符序列、效率高、线程不安全,底层使用char[]存储字符

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

image-20220326155311108

② StringBuffer类

StringBuffer源码

image-20220326152959933

源码分析

  • 对于String
1
2
String s1 = new String(); // 实质: char[] value = new char[0];
String s2 = new String("abc"); // 实质:char[] value = new char[]{'a', 'b', 'c'};
  • 对于StringBuffer

    • 构造器和有效长度:StringBuffer类不同于String,其对象必须使用构造器生成。有三个构造器:

      • StringBuffer():初始容量为16的字符串缓冲区
      • StringBuffer(int size):构造指定容量的字符串缓冲区
      • StringBuffer(String str):将内容初始化为指定字符串内容
      1
      2
      3
      4
      5
      6
      7
      StringBuffer sb1 = new StringBuffer(); // char[] value = new char[16];底层创建了一个长度是16的char数组value
      sb1.append('a'); // value[0] = 'a';
      sb1.append('b'); // value[1] = 'b';
      System.out.println(sb1.length()); // 返回count(有效字符的个数)为2

      StringBuffer sb2 = new StringBuffer("abc"); // char[] value = new char["abc".length() + 16];创建后额外加16个空间
      System.out.println(sb2.length()); // 返回count(有效字符的个数)为3
    • 扩容:如果要添加的数据对底层数组盛不下了,那就需要扩容

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      // 与扩容相关的源码
      public AbstractStringBuilder append(String str) {
      if (str == null) {
      return appendNull();
      }
      int len = str.length();
      ensureCapacityInternal(count + len);
      putStringAt(count, str);
      count += len;
      return this;
      }

      private void ensureCapacityInternal(int minimumCapacity) {
      // overflow-conscious code
      int oldCapacity = value.length >> coder;
      if (minimumCapacity - oldCapacity > 0) {
      value = Arrays.copyOf(value, newCapacity(minimumCapacity) << coder);
      }
      }

      private int newCapacity(int minCapacity) {
      // overflow-conscious code
      int oldCapacity = value.length >> coder;
      int newCapacity = (oldCapacity << 1) + 2;
      if (newCapacity - minCapacity < 0) {
      newCapacity = minCapacity;
      }
      int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;
      return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
      ? hugeCapacity(minCapacity)
      : newCapacity;
      }

      总结:默认情况下,扩容为原来容量的2倍+2,同时将原有数组的元素复制到这个扩容后的数组当中。

      指导意义:建议使用带参数的构造器StringBuffer(int capacity)StringBuilder(int capacity),然后再考虑线程安全的问题

常用方法

  • 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 indexOf(String str)
  • public String substring(int start,int end)
  • public int length()
  • public char charAt(int n )
  • public void setCharAt(int n ,char ch)

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test1() {
StringBuffer s1 = new StringBuffer("abc");
s1.append('1');
s1.append(1);
System.out.println(s1); // abc11

s1.delete(0, 1);
System.out.println(s1); // bc11

s1.replace(0, 1, "ab");
System.out.println(s1); // abc11

System.out.println(s1.reverse()); // 11cba
}

以上方法支持方法链

1
2
3
StringBuffer s1 = new StringBuffer("abc");
s1.append("d").append("e").reverse();
System.out.println(s1); // edcba
③ StringBuilder类

StringBuilder和StringBuffer底层实现基本一致,方法与StringBuffer也一致,故略。

三者效率对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void test2() {
//初始设置
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));
}
1
2
3
StringBuffer的执行时间:4
StringBuilder的执行时间:2
String的执行时间:188

例题

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test3() {
String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);

System.out.println(sb.length()); // 4
System.out.println(sb); // "null"

StringBuffer sb1 = new StringBuffer(str); // 报错java.lang.NullPointerException
System.out.println(sb1);
}

解析:

  • append()方法在添加null时,调用appendNull():该方法会把null看作是字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private AbstractStringBuilder appendNull() {
ensureCapacityInternal(count + 4);
int count = this.count;
byte[] val = this.value;
if (isLatin1()) {
val[count++] = 'n';
val[count++] = 'u';
val[count++] = 'l';
val[count++] = 'l';
} else {
count = StringUTF16.putCharsAt(val, count, 'n', 'u', 'l', 'l');
}
this.count = count;
return this;
}
  • 调用构造器,将null传入进去:
1
2
3
4
5
public StringBuffer(String str) {
// null.length报错,抛出空指针异常
super(str.length() + 16);
append(str);
}

2.2 日期时间相关的类

2.2.1 JDK8之前的API

① System类

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

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

  1. UTC(Coordinated Universal Time)
  2. GMT(Greenwich Mean Time)
  3. CST(Central Standard Time)

代码演示

1
2
3
4
5
@Test
public void test0() {
long time = System.currentTimeMillis();
System.out.println(time); // 1648282222985
}
② Date类

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

  • 构造器

    • 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是时间标准。
    • 其它很多方法都过时了。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test1() {
// 空参构造器
Date date1 = new Date();
// 默认调用date的toString方法
System.out.println(date1); // Sat Mar 26 16:14:53 CST 2022
System.out.println(date1.getTime()); // 1648282572968

// 带参构造器
Date date2 = new Date(1648282572968L);
System.out.println(date2); // Sat Mar 26 16:16:12 CST 2022
}

注意

  • java.sql.Date继承了java.util.Date,对应着数据库中的日期类型的变量。
1
2
3
4
5
@Test
public void test1() {
java.sql.Date date = new java.sql.Date(1648282572968L);
System.out.println(date); // 2022-03-26
}
  • java.util.Date —> java.util.Date
1
2
3
4
5
6
7
8
9
10
@Test
public void test1() {
// 方式一:强制转换
Date date1 = new java.sql.Date();
java.sql.Date date2 = (java.sql.Date) date1;

// 方式二:
Date date3 = new Date();
java.sql.Date date4 = new java.sql.Date(date3.getTime());
}
③ SimpleDateFormat类

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

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

格式化

  • SimpleDateFormat() :默认的模式和语言环境创建对象
  • public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象,该对象调用。其中pattern格式:

image-20220328141608526

  • public String format(Date date):方法格式化时间对象date

解析

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

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test2() throws ParseException {
// 实例化SimpleDateFormat,使用默认的构造器
SimpleDateFormat sdf = new SimpleDateFormat();
// 格式化日期
Date date = new Date();
// System.out.println(date);
String format = sdf.format(date);
System.out.println(format); // 2022/3/28 下午2:11

// 解析
String str = "22/3/28 上午11:43";
Date date1 = sdf.parse(str);
System.out.println(date1); // Mon Mar 28 11:43:00 CST 2022

// ----------------------------
// 按照指定的方式格式化和解析
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String format1 = sdf1.format(date);
System.out.println(format1); // 2022-03-28 14:18:45

// 解析:要求符合sdf识别的格式
Date date2 = sdf1.parse("2020-01-01 00:00:00");
System.out.println(date2); // Wed Jan 01 00:00:00 CST 2020
}
④ Calendar类

2.2.2 JDK8中新的日期时间API

① 新API的引入

如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:

  • 可变性:像日期和时间这样的类应该是不可变的。
  • 偏移性:Date中的年份是从1900开始的,而月份都从0开始。
  • 格式化:格式化只对Date有用,Calendar则不行。
  • 此外,它们也不是线程安全的;不能处理闰秒等。

第三次引入的API是成功的,并且Java 8中引入的java.time API 已经纠正了过去的缺陷,将来很长一段时间内它都会为我们服务。

新日期时间API

  • java.time – 包含值对象的基础包
  • java.time.chrono – 提供对不同的日历系统的访问
  • java.time.format – 格式化和解析时间和日期
  • java.time.temporal – 包括底层框架和扩展特性
  • java.time.zone – 包含时区支持的类

说明:大多数开发者只会用到基础包和format包,也可能会用到temporal包。因此,尽管有68个新的公开类型,大多数开发者,大概将只会用到其中的三分之一。

② LocalDate类

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

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

常用方法

image-20220328143438694

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test3() {
// 获取当前日期、时间、日期+时间
LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();

System.out.println(localDate); // 2022-03-28
System.out.println(localTime); // 14:36:49.369528800
System.out.println(localDateTime); // 2022-03-28T14:36:49.369528800

// of()
// 设置指定的年、月、日、时、分、秒,没有偏移量
LocalDateTime localDateTime1 = LocalDateTime.of(2020, 10,6, 13, 23, 59);
System.out.println(localDateTime1); // 2020-10-06T13:23:59

//getXXX():
System.out.println(localDateTime.getDayOfMonth()); // 28
System.out.println(localDateTime.getDayOfWeek()); // MONDAY
}
③ Instant类

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

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

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

常用方法

image-20220328144434887

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

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test4() {
// 获取UTC时区(本初子午线)的Instant类的对象
Instant instant = Instant.now();
System.out.println(instant); // 2022-03-28T06:46:17.166469400Z

// 加8个小时
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime); // 2022-03-28T14:49:04.847567700+08:00

// 获得时间戳
long milli = instant.toEpochMilli();
System.out.println(milli); // 1648450257945

// 通过时间戳创建Instant对象
Instant instant1 = Instant.ofEpochMilli(1648450257945L);
System.out.println(instant1);// 2022-03-28T06:50:57.945Z
}
④ DateTimeFormatter类

java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:

  • 预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
  • 本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
  • 自定义的格式。如:ofPattern("yyyy-MM-dd hh:mm:ss")

常用方法

  • ofPattern(String pattern) 静态方法 , 返 回 一 个 指 定 字 符 串 格 式 DateTimeFormatter
  • format(TemporalAccessor t) 格式化一个日期、时间,返回字符串
  • parse(CharSequence text) 将指定格式的字符序列解析为一个日期、时间

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void test5() {
// 方式一:预定义的标准格式
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
// 格式化:日期 --> 字符串
LocalDateTime localDateTime = LocalDateTime.now();
String str1 = formatter.format(localDateTime);
System.out.println("格式化之前: " + localDateTime); // 2022-03-28T14:58:27.893263500
System.out.println("格式化之后: " + str1); // 2022-03-28T14:58:27.8932635

// 解析
TemporalAccessor parse = formatter.parse("2022-03-28T14:58:27.8932635");
System.out.println(parse); // {},ISO resolved to 2022-03-28T14:58:27.893263500

// 方式二:本地相关的格式
DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
// 格式化
String str2 = formatter1.format(localDateTime);
System.out.println(str2); // 2022/3/28 下午3:01

// 方式三:自定义格式,重点
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化
String str3 = formatter2.format(localDateTime);
System.out.println(str3); // 2022-03-28 15:04:37
// 解析
TemporalAccessor parse1 = formatter2.parse("2021-01-01 13:00:01");
System.out.println(parse1); // {},ISO resolved to 2021-01-01T13:00:01
}
⑤ 其他API

2.3 比较器

在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。

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

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

2.3.1 自然排序

① 概述

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

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

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

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

  • String:按照字符串中字符的Unicode值进行比较
  • Character:按照字符的Unicode值来进行比较
  • 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
  • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
  • Date、Time等:后面的日期时间比前面的日期时间大
② 实例及使用
  • 像String,包装类等实现了Comparable接口,重写了compareTo()方法,给出了比较两个对象大小的方式。
  • 重写compareTo()的规则:
    • 如果当前对象this大于形参对象obj,则返回正整数
    • 如果当前对象this小于形参对象obj,则返回负整数
    • 如果当前对象this等于形参对象obj,则返回
  • 对于自定义类来说,如果需要排序,可以让自定义类重写Comparable接口,重写compareTo方法来指明如何排序。

String重写的compareTo()

1
2
3
4
5
6
7
8
9
10
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}

代码示例——String实现自然排序

1
2
3
4
5
6
7
@Test
public void test0() {
String[] arr = new String[]{"AA", "CC", "KK", "MM", "GG", "JJ", "DD"};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [AA, CC, DD, GG, JJ, KK, MM]

}

代码示例——自定义类实现自然排序

  • 要排序的商品类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
@AllArgsConstructor
@ToString
public class Goods implements Comparable{
private String name;
private double 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 -this.name.compareTo(goods.name);
}
// 方式二:
// return Double.compare(this.price, goods.price);
}
throw new RuntimeException("传入的数据类型不一致");
}
}
  • 测试方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test1() {
Goods[] goods = new Goods[5];
goods[0] = new Goods("lenovo", 34);
goods[1] = new Goods("dell", 43);
goods[2] = new Goods("xiaomi", 12);
goods[3] = new Goods("huawei", 65);
goods[4] = new Goods("microsoft", 43);

Arrays.sort(goods);
System.out.println(Arrays.toString(goods));
//[Goods(name=xiaomi, price=12.0),
// Goods(name=lenovo, price=34.0),
// Goods(name=microsoft, price=43.0),
// Goods(name=dell, price=43.0),
// Goods(name=huawei, price=65.0)]
}

2.3.2 定制排序

① 概述

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

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

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

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

② 实例及使用

代码示例——String类实现Comparator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2(){
String[] arr = new String[]{"AA", "CC", "KK", "MM", "GG", "JJ", "DD"};
Arrays.sort(arr, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
// 从大到小排序
return -o1.compareTo(o2);
}
});
System.out.println(Arrays.toString(arr));
// [MM, KK, JJ, GG, DD, CC, AA]
}

代码示例——自定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void test3(){
Goods[] goods = new Goods[6];
goods[0] = new Goods("lenovo", 34);
goods[1] = new Goods("dell", 43);
goods[2] = new Goods("xiaomi", 12);
goods[3] = new Goods("huawei", 65);
goods[4] = new Goods("microsoft", 43);
goods[5] = new Goods("microsoft", 12);

Arrays.sort(goods, new Comparator<Goods>() {
@Override
public int compare(Goods o1, Goods o2) {
// 先照产品名称从低到高排序
// 再按照价格从高到低进行排序
if (o1.getName().equals(o2.getName())){
return -Double.compare(o1.getPrice(), o2.getPrice());
}else{
return o1.getName().compareTo(o2.getName());
}
}
});
System.out.println(Arrays.toString(goods));
// [Goods(name=dell, price=43.0),
// Goods(name=huawei, price=65.0),
// Goods(name=lenovo, price=34.0),
// Goods(name=microsoft, price=43.0),
// Goods(name=microsoft, price=12.0),
// Goods(name=xiaomi, price=12.0)]
}
③ 二者对比
  • Comparable一旦指定,能够保证实现类的对象能在任何位置都可以比较大小
  • Comparator属于临时性的比较

2.4 System类

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

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

成员变量

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

成员方法

  • native long currentTimeMillis(): 该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。
  • void exit(int status): 该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。
  • void gc(): 该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。
  • String getProperty(String key): 该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:

image-20220328195543331

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test0(){
String javaVersion = System.getProperty("java.version");
System.out.println("java的version:" + javaVersion);
String javaHome = System.getProperty("java.home");
System.out.println("java的home:" + javaHome);
String osName = System.getProperty("os.name");
System.out.println("os的name:" + osName);
String osVersion = System.getProperty("os.version");
System.out.println("os的version:" + osVersion);
String userName = System.getProperty("user.name");
System.out.println("user的name:" + userName);
String userHome = System.getProperty("user.home");
System.out.println("user的home:" + userHome);
String userDir = System.getProperty("user.dir");
System.out.println("user的dir:" + userDir);
// java的version:11.0.10
// java的home:C:\Users\Hongyi\.jdks\corretto-11.0.10
// os的name:Windows 10
// os的version:10.0
// user的name:Hongyi
// user的home:C:\Users\Hongyi
// user的dir:E:\develop\study\backend_study\javase
}

3 枚举类和注解

3.1 枚举类的使用

3.1.1 概述

类的对象只有有限个,确定的。举例如下:

  • 星期:Monday(星期一)、……、Sunday(星期天)
  • 性别:Man(男)、Woman(女)
  • 季节:Spring(春节)……Winter(冬天)
  • 支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银
    行卡)、CreditCard(信用卡)
  • 就职状态:Busy、Free、Vocation、Dimission
  • 订单状态:Nonpayment(未付款)、Paid(已付款)、Delivered(已发货)、Return(退货)、Checked(已确认)Fulfilled(已配货)
  • 线程状态:创建、就绪、运行、阻塞、死亡

当需要定义一组常量时,强烈建议使用枚举类。

枚举类的实现

  • JDK1.5之前需要自定义枚举类

  • JDK1.5新增的 enum 关键字用于定义枚举类

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

枚举类的属性

  • 枚举类对象的属性不应允许被改动,所以应该使用 private final 修饰

  • 枚举类的使用 private final 修饰的属性应该在构造器中为其赋值

  • 若枚举类显式的定义了带参数的构造器,则在列出枚举值时也必须对应的传入参数

3.1.2 自定义枚举类

JDK1.5之前需要自定义枚举类

  • 私有化类的构造器,保证不能在类的外部创建其对象
  • 在类的内部创建枚举类的实例,声明为:public static final
  • 对象如果有实例变量,应该声明为private final,并在构造器中初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class EnumTest {
@Test
public void test0(){
System.out.println(Season.AUTUMN); // Season{seasonName='秋天', seasonDesc='秋高气爽'}
System.out.println(Season.SPRING);
}
}

// 定义枚举类
class Season{
// 1.声明Season对象的属性
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.获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}

// 5.toString

@Override
public String toString() {
return "Season{" +
"seasonName='" + seasonName + '\'' +
", seasonDesc='" + seasonDesc + '\'' +
'}';
}
}

3.1.3 使用Enum定义枚举类

① 使用说明
  • 使用 enum 定义的枚举类默认继承了 java.lang.Enum类,因此不能再继承其他类
  • 枚举类的构造器只能使用 private 权限修饰符
  • 枚举类的所有实例必须在枚举类中显式列出(,分隔 ;结尾)。对于列出的实例,系统会自动添加 public static final 修饰
  • 必须在枚举类的第一行声明枚举类对象

JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式,case 子句可以直接使用枚举值的名字,无需添加枚举类作为限定。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class EnumTest {
@Test
public void test0(){
System.out.println(Season.SPRING); // SPRING
System.out.println(Season.class.getSuperclass()); // class java.lang.Enum
}
}

enum Season{
// 1.提供当前枚举类的对象
// 多个对象之间用 , 隔开,最后一个用 ;
SPRING("春天", "春暖花开"),
SUMMER("夏天", "烈日当空"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "冰天雪地");


// 2.声明Season对象的属性
private final String seasonName;
private final String seasonDesc;

// 3.私有化类的构造器,并给对象属性赋值
private Season(String seasonName, String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}

// 4.获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}
}
② Enum类的主要方法
  • values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
  • valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException
  • toString():返回当前枚举类对象常量的名称

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void test1(){
Season summer = Season.SUMMER;
// 枚举类对象的toString方法
// 输出枚举类对象的名称
System.out.println(summer.toString()); // SUMMER
// values()返回枚举类型的对象数组
Season[] seasons = Season.values();
for(Season season:seasons){
System.out.println(season);
// SPRING
// SUMMER
// AUTUMN
// WINTER
}
Thread.State[] states = Thread.State.values();
for(Thread.State state:states){
System.out.println(state);
// NEW
// RUNNABLE
// BLOCKED
// WAITING
// TIMED_WAITING
// TERMINATED
}
//---------------------------------------------
Season winter = Season.valueOf("WINTER");
System.out.println(winter); // WINTER
}
③ 实现接口的枚举类
  • 和普通 Java 类一样,枚举类可以实现一个或多个接口
  • 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
  • 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法

代码示例

  • 情况一:若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class EnumTest {
@Test
public void test2(){
Season autumn = Season.AUTUMN;
autumn.show(); // 这是一个季节
}
}

interface Info{
void show();
}

enum Season implements Info{
// 1.提供当前枚举类的对象
// 多个对象之间用 , 隔开,最后一个用 ;
SPRING("春天", "春暖花开"),
SUMMER("夏天", "烈日当空"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "冰天雪地");


// 2.声明Season对象的属性
private final String seasonName;
private final String seasonDesc;

// 3.私有化类的构造器,并给对象属性赋值
private Season(String seasonName, String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}

// 4.获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}

@Override
public void show() {
System.out.println("这是一个季节");
}
}
  • 情况二:若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class EnumTest {
@Test
public void test2(){
Season autumn = Season.AUTUMN;
autumn.show(); // 这是秋天
}
}

interface Info{
void show();
}

enum Season implements Info{
// 1.提供当前枚举类的对象
// 多个对象之间用 , 隔开,最后一个用 ;
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("这是冬天");
}
};


// 2.声明Season对象的属性
private final String seasonName;
private final String seasonDesc;

// 3.私有化类的构造器,并给对象属性赋值
private Season(String seasonName, String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}

// 4.获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}
}

3.2 注解的使用

3.2.1 概述

JDK 5.0 开始,Java 增加了对元数据(MetaData) 的支持,也就是Annotation(注解) 。

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

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

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

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

3.2.2 常见的Annotation示例

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

示例1——生成文档相关的注解

  • @author 标明开发该类模块的作者,多个作者之间使用,分割
  • @version 标明该类模块的版本
  • @see 参考转向,也就是相关主题
  • @since 从哪个版本开始增加的
  • @param 对方法中某参数的说明,如果没有参数就不能写
  • @return 对方法返回值的说明,如果方法的返回值类型是void就不能写
  • @exception 对方法可能抛出的异常进行说明,如果方法没有用throws显式抛出的异常就不能写

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

  • @Override: 限定重写父类方法, 该注解只能用于方法
  • @Deprecated: 用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择。
  • @SuppressWarnings: 抑制编译器警告
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class AnnotationTest {
@Test
public void test0(){
Person p = new Person("Hongyi", 24);
p.eat();
// 抑制编译器警告:因为a没被使用
@SuppressWarnings("unused")
int a = 10;
}
}

class Person{
private String name;
private int age;

public Person() {

}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public void walk(){
System.out.println("walk...");
}

// 过时的方法
@Deprecated
public void eat(){
System.out.println("eat...");
}

}

interface Infor{
void show();
}

class Student extends Person implements Infor{
// 重写方法
@Override
public void walk() {
System.out.println("student walks...");
}

@Override
public void show() {
System.out.println("student shows...");
}
}

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

  • Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。
  • spring框架中关于“事务”的管理

3.2.3 自定义注解

① 基本使用
  • 定义新的 Annotation 类型使用 @interface 关键字
  • 自定义注解自动继承了java.lang.annotation.Annotation接口
  • Annotation 的成员变量在 Annotation 定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
  • 可以在定义 Annotation 的成员变量时为其指定初始值,指定成员变量的初始值可使用 default 关键字
  • 如果只有一个参数成员,建议使用参数名为value
  • 如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”
  • 没有成员定义的 Annotation 称为标记(例如@Override),包含成员变量的 Annotation 称为元数据Annotation
  • 注意:自定义注解必须配上注解的信息处理流程(使用反射)才有意义。

代码示例

  • 自定义注解类
1
2
3
public @interface MyAnnotation {
String value() default "world";
}
  • 使用
1
2
3
4
@MyAnnotation(value = "hello")
class Person{
// ...
}
② 基本元注解

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

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

  1. Retention
  2. Target
  3. Documented
  4. Inherited

@Retention

  • 只能用于修饰一个 Annotation 定义,用于指定该 Annotation 的生命周期,@Rentention 包含一个 RetentionPolicy 类型的成员变量,使用@Rentention 时必须为该 value 成员变量指定值
    • RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释。
    • RetentionPolicy.CLASS:在class文件中有效(即class保留),当运行 Java 程序时,JVM 不会保留注解。 这是默认值
    • RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时,JVM 会保留注释。程序可以通过反射获取该注释。

image-20220330133458366

1
2
3
4
5
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
// ...
}

@Target

用于修饰 Annotation 定义,用于指定被修饰的 Annotation 能用于修饰哪些程序元素。 @Target 也包含一个名为 value 的成员变量。

取值(ElementType) 说明
CONSTRUCTOR 构造器
FIELD
LOCAL_VIRIABLE 局部变量
METHOD 方法
PACKAGE
PARAMETER 参数
TYPE 类、接口、enum

@Documented

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

定义为Documented的注解必须设置Retention值为RUNTIME

@Inherited

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

4 集合

4.1 集合框架概述

集合,数组都是对多个数据进行存储的结构,简称Java容器。说明:此时的存储是内存层面的存储,不涉及持久化的存储。

java集合可分为Collection和Map两种体系。

image-20220330134625224

image-20220330134642074

  1. Collection接口:单列数据,定义了存取一组对象的方法和集合。
  • List:元素有序,可重复的集合
  • Set:元素无序,不可重复的集合
  1. Map接口:双列数据,保存具有映射关系Key-Value对的集合。

4.1.1 数组的特点

数组在存储多个数据方面的特点

  1. 一旦初始化以后,其长度就确定了

  2. 数组一旦定义好,其元素的类型也就确定了,例如String[] arr;Object[] arr等

数组在存储多个数据方面的缺点

  1. 一旦初始化后,其长度就无法修改

  2. 数组中提供的方法非常有限,对于添加,删除,插入数据等操作非常不便,同时效率不高。

  3. 对于获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用

  4. 数组存储数据的特点:有序,可重复。对于无序,不可重复的需求,数组不能满足。

4.1.2 集合框架

1
2
3
4
5
6
7
8
|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储有序的,可重复的数据。
|----ArrayList,LinkedList,Vector
|----Set接口:存储无序的,不可重复的数据。
|----HashSet,LinkedHashSet,TreeSet

|----Map接口:双列集合,用来存储一对一对的数据。
|----HashMap,LinkedHashMap,TreeMap,Hashtable,Properties

4.2 Collection接口

4.2.1 Collection接口中的常用方法

add()addAll()clear()size()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test1(){
// 以实现类ArrayList中的方法为例
Collection coll = new ArrayList();
// add(Object e):将元素e添加到集合coll中
coll.add("AAA");
coll.add(123);// 自动装箱
coll.add(new Date());

// size():获取添加的元素的个数
System.out.println(coll.size());// 3

// addAll(Collection coll1):将coll1中的元素添加到当前的集合中
Collection coll1 = new ArrayList();
coll1.add("BBB");
coll1.add(new Date());
coll.addAll(coll1);
System.out.println(coll.size());// 5

// clear():清空集合中的元素
// coll.clear();

// isEmpty():判断当前集合是否为空
System.out.println(coll.isEmpty());// false
}

contains()containsAll()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test2(){
Collection coll = new ArrayList();
coll.add("AAA");
coll.add(123);// 自动装箱
coll.add(456);
coll.add(new Date());
coll.add(new String("Tom"));// 字符串
coll.add(false);// 布尔类型(自动装箱)
coll.add(new Person("Jerry",20));// 添加自定义类

// contains(Object obj):判断当前集合中是否包含obj
// contains在判断时会调用obj对象所在类的equals方法。通常自定义的类都要重写equals方法。
boolean contains = coll.contains(123);
System.out.println(contains);// true
System.out.println(coll.contains(new String("Tom")));// true 判断的是内容,调用的是equals,而不是==
System.out.println(coll.contains(new Person("Jerry",20)));// false 如果重写Person类的equals则为true

// containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在与当前集合中
Collection coll1 = Arrays.asList(123,456);
System.out.println(coll.containsAll(coll1));// true
}

Person类中重写的equals方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// idea自动生成的重写Object类里的equals方法
@Override
public boolean equals(Object o) {
// 是同一个对象,则为真
if (this == o) return true;
// 比较对象为空,则为假
if (o == null || getClass() != o.getClass()) return false;
// 强转为Person
Person person = (Person) o;
// 先比较age字段
if (age != person.age) return false;
// 再比较name字段
return name != null ? name.equals(person.name) : person.name == null;
}

remove()removeAll()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);// 自动装箱
coll.add(456);
coll.add(new String("Tom"));// 字符串
coll.add(false);// 布尔类型(自动装箱)
coll.add(new Person("Jerry",20));// 添加自定义类

// remove(Object obj):移除集合中的指定元素,移除成功返回true,不成功为false
System.out.println(coll.remove(123));// true 表明已移除
System.out.println(coll.remove(new Person("Jerry", 20)));// true

// removeAll():差集:从当前集合中移除coll1中的所有的元素
Collection coll1 = Arrays.asList(123,456);
coll.removeAll(coll1);
System.out.println(coll);// [Tom, false]
}

retainAll()

1
coll.retainAll(coll1);// 获取coll和coll1的交集,并返回给coll

equals()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test4(){
// 注意ArrayList是有序的,比较时就要判断顺序
Collection coll = new ArrayList();
coll.add(123);// 自动装箱
coll.add(456);
coll.add(new String("Tom"));// 字符串
coll.add(false);// 布尔类型(自动装箱)
coll.add(new Person("Jerry",20));// 添加自定义类

Collection coll1 = new ArrayList();
coll1.add(123);// 自动装箱
coll1.add(456);
coll1.add(new String("Tom"));// 字符串
coll1.add(false);// 布尔类型(自动装箱)
coll1.add(new Person("Jerry",20));// 添加自定义类

// equals(Collection coll1):比较当前集合和coll1中的内容和对应顺序是否都一样
System.out.println(coll.equals(coll1));// true
}

hashCode()toArray()Arrays.asList()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test5(){
Collection coll = new ArrayList();
coll.add(123);// 自动装箱
coll.add(456);
coll.add(new String("Tom"));// 字符串
coll.add(false);// 布尔类型(自动装箱)
coll.add(new Person("Jerry",20));// 添加自定义类

// hashCode():返回当前对象的hash值
System.out.println(coll.hashCode());

// 集合转换为数组:toArray()
Object[] array = coll.toArray();
for(int i=0;i<array.length;i++){
System.out.println(array[i]);
}

// 数组转换为集合:调用Arrays的静态方法asList()
List<String> list = Arrays.asList(new String[]{"AAA", "BBB", "CCC"});
System.out.println(list);
}

4.2.2 集合元素的遍历

集合元素的遍历操作,需要使用Iterator接口。

  • 设计模式给迭代器模式的定义为:提供一种方法访问一个容器对象中的各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator方法,用以返回一个实现了Iterator接口的对象。
  • Iterator仅用于遍历集合,其本身不具有提供承载对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。
  • ==集合对象每次调用iterator方法都得到一个全新的迭代器对象==,默认游标都在集合的第一个元素之前

遍历代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Test
public void test6(){
Collection coll = new ArrayList();
coll.add(123);// 自动装箱
coll.add(456);
coll.add(new String("Tom"));// 字符串
coll.add(false);// 布尔类型(自动装箱)
coll.add(new Person("Jerry",20));// 添加自定义类
// 获取集合对象的迭代器
Iterator iterator = coll.iterator();
// 集合遍历:
// 方法1:不推荐
// 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
//System.out.println(iterator.next());

// 方法2:不推荐
Iterator iterator1 = coll.iterator();
for(int i=0;i<coll.size();i++){
System.out.println(iterator1.next());
}

// 方法3:推荐 hasNext()和next()搭配使用
Iterator iterator2 = coll.iterator();
while (iterator2.hasNext()){
// next():1.指针下移 2.将下移以后的位置上的元素返回
System.out.println(iterator2.next());
}
}

迭代器原理

image-20211215224454538

用迭代器遍历的错误写法

1
2
3
4
5
6
7
8
Iterator iterator = coll.iterator();
while((iterator.next()) != null){
System.out.println(iterator.next());
}// 导致间隔输出

while(coll.iterator().hasNext()){// 每次循环都会产生新的迭代器
System.out.println(iterator.next());
}// 死循环,且只输出第一个元素

迭代器的remove()

可以在遍历的时候删除集合中的元素。此方法不同于集合直接调用remove方法

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test7(){
Collection coll = new ArrayList();
coll.add(123);// 自动装箱
coll.add(456);
coll.add(new String("Tom"));// 字符串
coll.add(false);// 布尔类型(自动装箱)
coll.add(new Person("Jerry",20));// 添加自定义类

// 删除集合中"Tom"字符串
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
Object obj = iterator.next();
if("Tom".equals(obj)){
iterator.remove();
}
}
// 重新遍历集合
iterator = coll.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

注意:如果还未调用next就调用remove,或者调用一次remove后再次调用remove,会报IllegalStateException异常。

4.2.3 foreach循环遍历

jdk5.0新增特性,用于遍历数组和集合。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test(){
Collection coll = new ArrayList();
coll.add(123);// 自动装箱
coll.add(456);
coll.add(new String("Tom"));// 字符串
coll.add(false);// 布尔类型(自动装箱)
coll.add(new Person("Jerry",20));// 添加自定义类

// for(集合中元素的类型 局部变量:集合对象){ ... }
// 内部仍然调用了迭代器
for(Object obj : coll){
System.out.println(obj);
}

// foreach遍历数组
int arr[] = new int[]{1,2,3,4,5,6,7};
for (int i: arr) {
System.out.println(i);
}
}

一个练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test1(){
String arr[] = new String[]{"MM","MM","MM"};

// 方式1:普通for赋值,此时能够改变原始数组
for (int i=0;i<arr.length;i++){
arr[i] = "GG";
}

// 方式2:增强for循环,此时只能改变局部变量s的值,原始数组不变
for (String s : arr){
s = "GG";
}

// 打印
for (int i=0;i<arr.length;i++){
System.out.println(arr[i]);
}
}

4.3 List接口及其实现类

4.3.1 List概述

  • List接口是Collection的子接口,通常使用List来替代数组。
  • List集合中的元素有序且可重复,每个元素都有对应的索引顺序,可根据整型序号来对元素进行存取。
  • List接口的常用实现类有ArrayListLinkedListVector

面试题:实现类三者的异同?

同:都是List接口的实现类,存储的都是有序可重复的数据。

异:

1
2
3
4
5
|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储有序的,可重复的数据。“动态数组”
|----ArrayList:JDK1.2,作为List接口的主要实现类,线程不安全的,效率高;底层采用Object[] elementData存储(顺序表),相当于C++中的vector
|----LinkedList:JDK1.2,底层采用的双向链表存储,对于频繁的插入和删除操作的效率比上者高。相当于C++中的list
|----Vector:JDK1.0,是List接口的古老实现类,线程安全的,效率低

4.3.2 ArrayList源码分析

JDK7版本——饿汉式

1
2
3
4
ArrayList list = new ArrayList();// 底层创建了长度是10的Object[]数组elementData
list.add(123);// elementData[0] = new Integer(123);
//...一系列添加操作
list.add(123);// 如果此次的添加导致elementData容量不够,则扩容,默认扩容为原来容量的1.5倍(新造一个数组),同时需要将原有的数据复制到新的数组中

结论:实际开发中使用带参的构造器,指定出大小:

1
ArrayList list = new ArrayList(int capacity);

JDK8版本——懒汉式

1
2
3
ArrayList list = new ArrayList();// 底层Object[] elementData初始化为{},并没有创建长度为10的数组
list.add(123);// 第一次调用add()时,底层才创建长度为10的数组,并将数据添加进去
// 后续的添加和扩容操作与jdk7无异

默认构造函数:

1
2
3
4
5
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

添加一个元素:

1
2
3
4
5
6
7
8
9
// s: size,当前的大小
// elementData: 底层数组
// e: 添加的元素
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow(); // 扩容
elementData[s] = e; // 设置元素
size = s + 1; // 大小+1
}

扩容函数:

1
2
3
4
5
6
7
8
private Object[] grow() {
return grow(size + 1); // 至少扩容至size+1
}

private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}

可以看出调用了Arrays.copyOf复制了一份elementData,并且设置容量为newCapacity(minCapacity)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 计算新的容量
// minCapacity = size + 1
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// oldCapacity右移1位,相当于除以2
// newCapacity = oldCapacity + oldCapacity / 2,为原来大小的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}

小结

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

4.3.3 LinkedList源码分析

JDK8版本

  • 结点结构:
1
2
3
4
5
6
7
8
9
10
11
12
// 作为LinkedList类的内部类
private static class Node<E> {
E item;
LinkedList.Node<E> next;
LinkedList.Node<E> prev;// 体现了双向链表
// 构造器
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
  • 添加元素(连接在链表尾部)
1
2
3
4
5
6
7
8
9
10
11
12
13
void linkLast(E e) {
LinkedList.Node<E> l = this.last;
LinkedList.Node<E> newNode = new LinkedList.Node(l, e, (LinkedList.Node)null);
this.last = newNode;
if (l == null) {// 链表为空
this.first = newNode;// 作为第一个结点
} else {// 链表不空
l.next = newNode;// 连接在尾部
}

++this.size;
++this.modCount;
}
  • 分析
1
2
3
LinkedList list = new LinkedList();// 内部声明了Node类型的first和last属性,默认值为null
list.add(123);// 将123封装到Node对象
// 不涉及扩容

4.3.4 List接口的常用方法

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

代码演示说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Test
public void test(){
ArrayList list = new ArrayList();
list.add(123);// 自动装箱
list.add(456);
list.add(new String("Tom"));// 字符串
list.add(false);// 布尔类型(自动装箱)
list.add(new Person("Jerry",20));// 添加自定义类

// void add(int index,Object ele):在index位置上插入ele
list.add(1,"BB");
System.out.println(list);//[123, BB, 456, Tom, false, Person{name='Jerry', age=20}]

// boolean addAll(int index,Collection eles):从index位置开始将eles的所有元素添加到list中
List list1 = Arrays.asList("hongyi",true);
list.addAll(1,list1);
System.out.println(list);//[123, hongyi, true, BB, 456, Tom, false, Person{name='Jerry', age=20}]

// Object get(int index):获取index位置的元素
System.out.println(list.get(1));// hongyi

// int indexOf(Object obj):返回obj在集合中首次出现的索引
// 没有返回-1
System.out.println(list.indexOf("BB"));// 3

// int lastIndexOf(Object obj):返回obj在集合中最后出现的索引

// Object remove(int index):移除index位置上的元素,并返回此元素
// 注意这个方法重载了Collection的remove方法
System.out.println(list.remove(0));// 123

// Object set(int index,Object obj):将index位置上的元素更改为obj

// List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置上的子集合
// 左闭右开
List list2 = list.subList(1,4);
System.out.println(list2);//[true, BB, 456]
}

总结常用方法

  • 增:add(Object obj)

  • 删:remove(int index)/remove(Object obj)

  • 改:set()

  • 查:get()

  • 插:add()

  • 长度:size()

  • 遍历:1.Iterator迭代器 2.增强for循环 3.普通循环

4.3.5 List集合的遍历

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test1(){
ArrayList list = new ArrayList();
list.add(123);// 自动装箱
list.add(456);
list.add(new String("Tom"));// 字符串
list.add(false);// 布尔类型(自动装箱)
list.add(new Person("Jerry",20));// 添加自定义类

// 方法1:迭代器
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}

// 方法2:增强for循环
for(Object obj : list){
System.out.println(obj);
}

// 方法3:普通for循环
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}

一道笔试题

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2(){
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list);// [1, 2]
}

private static void updateList(List list){
list.remove(2);
}

注意区分List中的remove方法

4.4 Set接口及其实现类

4.4.1 Set概述

Set接口中没有额外定义新的方法,使用的都是Collection接口定义的方法

框架

1
2
3
4
5
|----Collection接口:单列集合,用来存储一个一个的对象
|----Set接口:存储无序的,不可重复的数据。
|----HashSet:作为Set的主要实现类;线程不安全,可以存储null值
|----LinkedHashSet:作为HashSet的子类;遍历其内部数据时可以按照添加的顺序去遍历
|----TreeSet:可以按照添加的对象的指定属性进行排序,底层采用红黑树

无序和无可重复的理解

  • 无序性:不等于随机性。以HashSet为例,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值添加。
  • 不可重复性:保证添加的元素按照equal方法判断时不能返回true,即相同的元素只能添加一个。

4.4.2 HashSet添加元素的过程

向HashSet中添加元素a,首先,调用a的所在类的hashCode方法,计算出a元素的哈希值,此哈希值接着通过某种算法(例如除留余数法)计算出a在HashSet底层数组中的存放位置,即为索引位置。判断数组此位置上是否已经含有元素:

|—-如果此位置上没有其他元素,则a添加成功;—->情况1

|—-如果此位置上有其他元素b(或以链表形式存在的多个元素),首先比较a和b的哈希值:

​ |—-如果哈希值不相同,则a添加成功;—->情况2

​ |—-如果哈希值相同,则需要调用a所在类的equals方法,与链表上的元素逐一相比:

​ |—-如果一旦返回true,则添加失败;

​ |—-如果比较到最后返回false,则a添加成功;—->情况3

对于添加成功的情况2和情况3,元素a与已经存在指定索引位置上的数据以链表方式进行存储。在JDK7中,元素a放在数组中,指向原来的元素链;在JDK8中,原来的链尾元素指向新添加进来的元素a。(==7上8下==)

HashSet的底层为:==数组+链表==,实质上还是new了一个HashMap

4.4.3 关于equals()hashCode()方法的重写

idea中自动生成的重写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

User user = (User) o;

if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}

@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}

选择31作为乘数的原因:选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)。并且31只占用5bits,相乘造成数据溢出的概率较小。31可以由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)。31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

重写原则

  • 向set添加的元素所在的类必须重写hashCode和equals方法。

  • 重写的两个方法要保持一致性:相等的对象必须有相等的哈希值

重写hashCode方法的原则

  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。

  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。

  • 对象中用作 equals() 方法比较的属性,都应该用来计算 hashCode 值。

重写equals方法的原则

当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象。

因此,违反了“相等的对象必须具有相等的散列码”。

结论:==复写equals方法的时候一般都需要同时复写hashCode方法==。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

4.4.4 LinkedHashSet的使用

LinkedHashSet是作为HashSet的子类,在添加数据的同时,还维护了每个数据的添加的先后顺序。即每个结点有前后两个指针域,指示上一个和下一个元素的位置。对于频繁的遍历操作,效率比HashSet高。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// LinkedHashSet的使用
@Test
public void test2(){
Set set = new LinkedHashSet();
set.add(123);
set.add(456);
set.add("AA");
set.add("CC");
set.add(new User("Tom",23));
set.add(new User("Tom",23));// 重复元素不会被添加进集合
set.add(false);

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

image-20211215224525688

原理示意图

image-20211215224534048

4.4.5 TreeSet的使用

  1. 向TreeSet中添加的数据,要求是==相同类的对象==
1
2
3
4
5
6
7
8
@Test
public void test3(){
Set set = new TreeSet();
set.add(123);
set.add(456);
set.add("AA");// 此处就会报错,不能添加不同类的对象
set.add(new User("Tom",23));// 这里也会报错
}
  1. 可以按照排序后输出
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test3(){
Set set = new TreeSet();
set.add(13);
set.add(6);
set.add(-1);

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

image-20211215224543201

  1. TreeSet的底层是==红黑树==

TreeSet的自然排序(要求比较对象所在类实现Comparable接口)

在TreeSet自然排序中,比较两个对象是否相同的标准为:compareTo()返回值为0,不再是用equals()判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test3(){
Set set = new TreeSet();

set.add(new User("Tom",23));
set.add(new User("Jerry",32));
set.add(new User("Mike",53));
set.add(new User("Mike",2));

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class User implements Comparable{
private String name;
private int age;

// 按照姓名,年龄从小到大排列
public int compareTo(Object o) {
if (o instanceof User){
User user = (User)o;
int compare = this.name.compareTo(user.name);
if(compare != 0){// 先按照姓名排序
return compare;
}else{// 再按照年龄排序
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}

image-20211215224551930

TreeSet的定制排序(要求TreeSet的构造器参数为实现Comparator接口的对象)

在TreeSet定制排序中,比较两个对象是否相同的标准为:compare()返回值为0,不再是用equals()判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void test4(){
Comparator com = new Comparator() {
// 按照年龄从小到大排列
@Override
public int compare(Object o, Object t1) {
if(o instanceof User && t1 instanceof User){
User u1 = (User)o;
User u2 = (User)t1;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
Set set = new TreeSet(com);// 定制排序

set.add(new User("Tom",23));
set.add(new User("Jerry",32));
set.add(new User("Mike",53));
set.add(new User("Mary",53));// 添加不进去,因为按照age定制排序且age相同
set.add(new User("Mike",2));

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

image-20211215224559647

4.5 Map接口及其实现类

4.5.1 Map框架概述

1
2
3
4
5
6
|----Map接口:双列集合,用来存储一对一对的数据。 
|----HashMap:作为Map的主要实现类。线程不安全,效率高。可以存储null的key和value。
|----LinkedHashMap:保证在遍历Map元素时是按照添加的顺序实现遍历。在原有的HashMap的底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类的执行效率要高于HashMap。
|----TreeMap:保证按照添加的键值对进行排序,实现排序遍历。此时考虑key的自然或定制排序。底层是红黑树。
|----Hashtable:注意t小写。Map的古老实现类。线程安全,效率低。不能存储null的key和value。
|----Properties:常用来处理配置文件。key和value都是String类型。

HashMap底层

  • jdk7数组+链表

  • jdk8数组+链表+==红黑树==

典型面试题

  1. HashMap的底层实现原理

  2. HashMap和Hashtable的异同

常用方法

image-20230620170646568

4.5.2 Map结构的理解:key和value的特点

image-20211215224608289

  • Map中的key:无序不可重复,使用Set存储所有的key;要求key所在类重写equals和hashCode方法。

  • Map中的value:无序可重复,使用Collection存储所有的value;要求key所在类重写equals方法。

  • 一个键值对构成了一个Entry对象

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

4.5.3 HashMap

JDK7版本

底层数据结构:Entry数组和链表

1
HashMap map = new HashMap();

在实例化以后,底层创建了长度是==16==的一维数组Entry[] table

1
2
// 可能已经执行过多次put操作
map.put(key1,value1);

首先,调用key1所在类的hashCode方法计算key1的哈希值,此哈希值经过某种算法后(例如取余),得到在Entry数组中的存放位置。

​ |——如果此位置上为空,则entry对象添加成功;情况1

​ |——如果此位置上不为空,意味着此位置上存在一个或多个数据(以链表形式存在),则比较key1和已经存在的数据的哈希值:

​ |——如果都不相同,则添加成功,进行头插;情况2

​ |——如果与某一个数据的哈希值相同,则调用key1所在类的equals方法进行比较:

​ |——如果返回false,则添加成功,进行头插;情况3

​ |——如果返回true,使用value1==替换==相同key的value值。

  • 对于情况2和情况3:同HashSet一样七上八下。
  • 扩容:默认的扩容方式为扩容为原来容量的==2倍==(即新开辟一个原来容量2倍的数组空间),并将原有数据复制到该新数组中(再散列,或者rehash)。
  • 哈希冲突时采用链地址法

image-20211215224618828

JDK8版本

底层数据结构:Node数组 + 链表 + 红黑树

1
HashMap map = new HashMap();
  • 底层没有创建一个长度为16的数组,并且该数组类型不是Entry了,而是Node;
  • 首次调用put方法时,才创建数组。(懒加载)
  • 底层结构新增了==红黑树==。当数组的某一个索引位置上的元素以链表形式存在的==个数大于8且当前数组的长度大于64时==,此索引位置上的所有数据改为使用红黑树存储

image-20211215224628060

4.5.4 TreeMap

TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序,也可以通过构造时传入的比较器。

TreeMap底层通过红黑树实现,算法复杂度为O(logN)

image-20231029162742964

4.5.5 LinkedHashMap

LinkedHashMap实现了Map接口,即允许放入keynull的元素,也允许插入valuenull的元素。

HashMap的区别在于,在HashMap的基础上,采用双向链表的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同

image-20231029161853939

put一个元素时:

  1. table的角度看,新的entry需要插入到对应的bucket里,当有哈希冲突时,采用头插法将新的entry插入到冲突链表的头部。
  2. header的角度看,新的entry需要插入到双向链表的尾部。

image-20231029162314283

4.5.6 ConcurrentHashMap

详见[[Java并发编程学习笔记#8.1 ConcurrentHashMap]]

4.6 Collections工具类

4.6.1 介绍

Collections 是一个操作 Set、List 和 Map 等集合的工具类。

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

4.6.2 常用方法

① 排序
  • reverse(List):反转 List 中元素的顺序
  • shuffle(List):对 List 集合元素进行随机排序
  • sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
  • sort(List, Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  • swap(List, int i, int j):将指定 list 集合中的 i 处元素和 j 处元素进行交换

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test0(){
List<Integer> list = new ArrayList<>();
list.add(123);
list.add(43);
list.add(0);
list.add(-97);
list.add(765);
System.out.println(list); // [123, 43, 0, -97, 765]
// 倒序处理
Collections.reverse(list);
System.out.println(list); // [765, -97, 0, 43, 123]
// 升序排序
Collections.sort(list);
System.out.println(list); // [-97, 0, 43, 123, 765]
// 定制排序
Collections.sort(list, new Comparator<Integer>() {
// 按照降序排序
@Override
public int compare(Integer o1, Integer o2) {
return -Integer.compare(o1, o2);
}
});
System.out.println(list); // [765, 123, 43, 0, -97]
}
② 查找和替换
  • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
  • Object min(Collection)
  • Object min(Collection, Comparator)
  • int frequency(Collection, Object):返回指定集合中指定元素的出现次数
  • void copy(List dest, List src):将src中的内容复制到dest中
  • boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List 对象的所有旧值

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test1(){
List<Integer> list = new ArrayList<>();
list.add(123);
list.add(43);
list.add(0);
list.add(-97);
list.add(765);
// 错误的写法:Source does not fit in dest
// List<Integer> dest = new ArrayList<>();
// Collections.copy(dest, list);

List<Integer> dest = Arrays.asList(new Integer[list.size()]);
System.out.println(dest.size()); // 5
Collections.copy(dest, list);
System.out.println(dest); // [123, 43, 0, -97, 765]
}
③ 同步控制

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

代码示例

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test2(){
List<Integer> list = new ArrayList<>();
list.add(123);
list.add(43);
list.add(0);
list.add(-97);
list.add(765);
// 返回一个线程安全的list1
List<Integer> list1 = Collections.synchronizedList(list);
}

5 泛型

5.1 概念

5.1.1 泛型的设计背景

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。因为这个时候除了元素的类型不缺定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection<E>List<E>ArrayList<E> 这个`就是类型参数,即泛型。

5.1.2 泛型的概念

  • 泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
  • 从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List<String>,这表明该List只能保存字符串类型的对象。
  • JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。

5.1.3 需要泛型的理由

  • 为什么要有泛型呢,直接Object不是也可以存储数据吗?
    • 解决元素存储的安全性问题,好比商品、药品标签,不会弄错。
    • 解决获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别。

image-20220331163204003

image-20220331163213755

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test0(){
// 集合中使用泛型之前的情况
ArrayList list = new ArrayList();
// 存放学生的成绩
list.add(12);
list.add(43);
list.add(89);
list.add(100);
list.add(56);
// 问题1: 类型不安全
list.add("Tom");

for(Object score:list){
// 问题2: 强制转换时,可能出现ClassCastException
int stuScore = (int) score;
System.out.println(score);
}
}

5.2 集合中使用泛型

以ArrayList和HashMap为例

5.2.1 ArrayList

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void test1(){
// 在集合中使用泛型
ArrayList<Integer> list = new ArrayList<Integer>();
// 存放学生的成绩
list.add(12);
list.add(43);
list.add(89);
list.add(100);
list.add(56);
// 编译时就会进行类型检查,保证数据的安全
// list.add("Tom");

// 方式一:增强for
for(Integer score:list){
// 避免了强转操作
int stuScore = score;
System.out.println(stuScore);
}

// 方式二:迭代器
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

5.2.2 HashMap

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test2(){
HashMap<String, Integer> map = new HashMap<>();
map.put("Tom", 97);
map.put("Jack", 100);
map.put("Hongyi", 100);

// 编译错误
// map.put(12, 12);

// 泛型的嵌套
Set<Map.Entry<String, Integer>> entries = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();
while(iterator.hasNext()){
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + "----" + value);
// Tom----97
// Jack----100
// Hongyi----100
}
}

5.2.3 总结

  • 集合接口和集合类在jdk5.0时都修改为带泛型的结构

  • 在实例化集合类时,可以指定具体的泛型类型

  • 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(例如方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型

    • 例如add(E e) —>实例化以后: add(Integer e)
  • 注意:泛型的类型必须是,不能是基本数据类型

    1
    2
    ArrayList<int> list = new ArrayList<>(); // 错误!
    ArrayList<Integer> list = new ArrayList<>(); // 正确!
  • 如果实例化时没有指定泛型,默认类型为Object类型

5.3 自定义泛型结构

5.3.1 自定义泛型类和接口

  • 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>

    • 例如Map的源码

      1
      2
      3
      public interface Map<K, V> {
      // ...
      }
  • 泛型类的构造器如下:public GenericClass(){}。而下面是错误的:public GenericClass<E>(){}

  • 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。

  • 泛型不同的引用不能相互赋值。

    1
    2
    3
    4
    // 下面的做法是错误的
    ArrayList<String> list1 = null;
    ArrayList<Integer> list2 = null;
    list1 = list2;
  • 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。

  • 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

  • jdk1.7,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>();(类型推断)

  • 泛型的指定中不能使用基本数据类型,可以使用包装类替换。

  • 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型

  • 异常类不能是泛型的。

    1
    2
    3
    4
    // 错误
    public class MyException<T> extends Exception{
    // ...
    }
  • 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现
      • 没有类型 擦除
      • 具体类型
    • 子类保留父类的泛型:泛型子类
      • 全部保留
      • 部分保留
    • 结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Father<T1, T2> {

}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {

}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {

}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Father<T1, T2> {

}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son<A, B> extends Father{
//等价于class Son extends Father<Object,Object>{
}
// 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> {

}

代码示例

  • 自定义泛型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 自定义泛型类
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;
}

@Override
public String toString() {
return "Order{" +
"orderName='" + orderName + '\'' +
", orderId=" + orderId +
", orderT=" + orderT +
'}';
}

// 静态方法中不能使用类的泛型,
// 因为静态方法的使用早于实例对象的创建
// 而此时泛型还尚未确定
public static void show() {
// 报错
// System.out.println(orderT);
}
}
  • 两个子类
1
2
3
public class SubOrder extends Order<Integer>{ // 不再是一个泛型类

}
1
2
3
public class SubOrder1<T> extends Order<T>{ // 仍然是一个泛型类

}
  • 测试方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test3(){
// 如果定义了泛型类,实例化没有指定类的泛型,则认为泛型类型为Object
// 要求:如果定义了类是带泛型的,建议在实例化时要指明类的泛型
// 下面的做法不建议
Order order = new Order();
order.setOrderT(123);
order.setOrderT("ABC");

// 建议使用泛型
Order<String> order1 = new Order<>("phone", 1001, "这是一部手机");
order1.setOrderT("又被修改了");
System.out.println(order1); // Order{orderName='phone', orderId=1001, orderT=又被修改了}

SubOrder sub1 = new SubOrder();
sub1.setOrderT(123);
System.out.println(sub1); // Order{orderName='null', orderId=0, orderT=123}

SubOrder1<String> sub2 = new SubOrder1<>();
sub2.setOrderT("Sub2...");
System.out.println(sub2); // Order{orderName='null', orderId=0, orderT=Sub2...}

}

5.3.2 自定义泛型方法

方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。

1
2
3
4
泛型方法的格式:
[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
例如:
public <E> List<E> copyFromArrayToList(E[] arr)

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 自定义泛型类
public class Order<T> {

String orderName;
int orderId;

// 类的内部结构就可以使用类的泛型
T orderT;
public Order(){

}
// 泛型方法:在方法中出现了泛型的结构,泛型的参数与类的参数没有任何关系
// 泛型方法所属的类是否是泛型类都没有关系
public <E> List<E> copyFromArrayToList(E[] arr) {
ArrayList<E> list = new ArrayList<>();
for(E e : arr) {
list.add(e);
}
return list;
}
}
  • 测试方法
1
2
3
4
5
6
7
@Test
public void test4() {
Order<String> order = new Order<>();
Integer[] arr = new Integer[]{1, 2, 3, 4};
List<Integer> list = order.copyFromArrayToList(arr);
System.out.println(list); // [1, 2, 3, 4]
}

5.4 泛型在继承上的体现

如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<B>并不是G<A>的子类型!

比如:String是Object的子类,但是List<String>并不是List<Object>的子类。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test5() {
Object obj = null;
String str = null;
obj = str;

Object[] arr1 = null;
String[] arr2 = null;
arr1 = arr2;

List<Object> list1 = null;
List<String> list2 = null;
// 编译错误,list1和list2的类型不具有子父类关系
// list1 = list2;
}

5.5 通配符

  • 使用类型通配符:?
    • 比如:List<?>Map<?,?>List<?>List<String>List<Object>等各种泛型List的父类
  • 读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。
  • 写入list中的元素时,不行。因为我们不知道?的元素类型,我们不能向其中添加对象。唯一的例外是null,它是所有类型的成员。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Test
public void test6() {
List<Object> list1= new ArrayList<>();
list1.add("ABC");
list1.add(123);
List<String> list2 = null;

List<?> list = null;

// 此时list可看作是所有List泛型实现类的父类
list = list1;
list = list2;

print(list1);
// ABC
// 123

List<String> list3 = new ArrayList<>();
list3.add("AA");
list3.add("BB");
list3.add("CC");
list = list3;
// 此时不能向list中添加数据
// 除了null
// list.add("DD");
list.add(null);
// 读取
Object o = list.get(0);
System.out.println(o); // AA
System.out.println(list); // [AA, BB, CC, null]
}

public void print(List<?> list) {
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}

6 IO流

6.1 File类

6.1.1 介绍

  • java.io.File类:文件和文件目录路径的抽象表示形式,与平台无关
  • File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。
  • 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
  • File对象可以作为参数传递给流的构造器

6.1.2 常用构造器

  • 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对象

路径分隔符

  • 路径中的每级目录之间用一个路径分隔符隔开。

  • 路径分隔符和系统有关:

    • windows和DOS系统默认使用\来表示
    • UNIX和URL使用/来表示
  • Java程序支持跨平台运行,因此路径分隔符要慎用。

  • 为了解决这个隐患,File类提供了一个常量:public static final String separator。根据操作系统,动态的提供分隔符。

    1
    2
    3
    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");

代码示例

1
2
3
4
5
6
7
8
9
10
@Test
public void test0() {
// 相对路径
File file1 = new File("./hello.txt");
// 绝对路径
File file2 = new File("E:\\develop\\study\\" +
"backend_study\\javase\\src\\main\\resource\\hello.txt");
System.out.println(file1); // .\hello.txt
System.out.println(file2);
}

6.1.3 常用方法

① 获取功能
  • 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数组

代码示例1

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test1() {
File file1 = new File("hello.txt");
File file2 = new File("E:\\develop\\study\\" +
"backend_study\\javase\\src\\main\\resources\\hello.txt");
System.out.println(file1.getAbsolutePath()); // E:\develop\study\backend_study\javase\hello.txt
System.out.println(file1.getPath()); // hello.txt
System.out.println(file1.getName()); // hello.txt
System.out.println(file1.getParent()); // null
System.out.println(file1.length()); // 3
System.out.println(file1.lastModified()); // 1648980340094
}

代码示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test2() {
File file = new File("E:\\develop\\study\\backend_study\\javase");
String[] list = file.list();
for(String l : list) {
System.out.println(l);
// .idea
// hello.txt
// pom.xml
// src
// target
}

File[] files = file.listFiles();
for(File f : files) {
System.out.println(f);
// E:\develop\study\backend_study\javase\.idea
// E:\develop\study\backend_study\javase\hello.txt
// E:\develop\study\backend_study\javase\pom.xml
// E:\develop\study\backend_study\javase\src
// E:\develop\study\backend_study\javase\target
}
}
② 重命名功能
  • public boolean renameTo(File dest):把文件重命名为指定的文件路径

代码示例

1
2
3
4
5
6
7
8
9
@Test
public void test3() {
// 需要file1在硬盘中存在,且file2不能在硬盘中存在
File file1 = new File("hello.txt");
File file2 = new File("E:\\develop\\study\\hi.txt");

boolean b = file1.renameTo(file2);
System.out.println(b);
}

执行后,当前路径的hello.txt已经转移至E:\develop\study\,且重命名为hi.txt

③ 判断功能
  • public boolean isDirectory():判断是否是文件目录
  • public boolean isFile() :判断是否是文件
  • public boolean exists() :判断是否存在
  • public boolean canRead() :判断是否可读
  • public boolean canWrite() :判断是否可写
  • public boolean isHidden() :判断是否隐藏

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test4() {
File file1 = new File("hello.txt");
System.out.println(file1.isDirectory());
System.out.println(file1.isFile());
System.out.println(file1.exists());
System.out.println(file1.canRead());
System.out.println(file1.canWrite());
System.out.println(file1.isHidden());
// false
// true
// true
// true
// true
// false
}
④ 创建功能
  • public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false
  • public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
  • public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建
⑤ 删除功能
  • public boolean delete():删除文件或者文件夹
    • 删除注意事项:Java中的删除不走回收站。 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录

6.2 IO流原理及流的分类

6.2.1 概述

I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

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

java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。硬盘 —> 内存
  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。 内存 —> 硬盘

应该在程序(内存)的角度看IO流的方向

6.2.2 流的分类

  • 按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)
  • 按数据流的流向不同分为:输入流,输出流
  • 按流的角色的不同分为:节点流,处理流(包在节点流外层)
抽象基类 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

Java的IO流共涉及40多个类,实际上非常规则,都是从如上4个抽象基类派生的。

由这四个类派生出来的子类名称都是以其父类名作为子类名后缀

image-20220404145113587

6.2.3 流的体系

image-20220404145208175

表格中第二行为节点流(或称为文件流),剩余的都为处理流。

6.2.4 节点流和处理流

  • 节点流:直接从数据源或目的地读写数据,或称为文件流

image-20220404145632710

  • 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

image-20220404145642301

6.2.5 InputStream & Reader

InputStream(典型实现:FileInputStream) 和 Reader(典型实现:FileReader) 是所有输入流的基类。

程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件 IO 资源

6.2.6 OutputStream & Writer

OutputStream 和 Writer 也非常相似。

因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,即以 String 对象作为参数。

FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter。

6.3 节点流(文件流)

6.3.1 字符流读取文件

  • Reader:输入字符流

    • int read():读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1

    • int read(char[] cbuf):将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。

    • int read(char[] cbuf,int off,int len):将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。

    • public void close() throws IOException:关闭此输入流并释放与该流关联的所有系统资源。

代码示例1

需求:在项目下创建一个文件hello.txt,文件内容为hello world,以字符流FileReader读取该文件到内存中并输出到控制台中显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void test0() {
FileReader fr = null;
try {
// 1.实例化File类的对象,指明要操作的文件
File file = new File("hello.txt");
// 2.提供具体的流
fr = new FileReader(file);
// 3.数据的读入
// 方式一:
// int data = fr.read();
// while (data != -1) {
// System.out.print((char) data);
// // 再次读取下一个字符
// data = fr.read();
// } // hello world

// 方式二:语法上的修改
int data;
while ((data = fr.read()) != -1) {
System.out.print((char) data);
}

} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fr != null) {
// 4.关闭流
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

注意:

  • 为了保证流资源一定可以被释放掉,需要使用tcf处理
  • 读入的文件一定要存在,否则会报FileNotFoundException

代码示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 对read()操作升级,使用read的重载方法
@Test
public void test1() {
FileReader fr = null;
try {
// 1.File类的实例化
File file = new File("hello.txt");
// 2.FileReader流的实例化
fr = new FileReader(file);
// 3.读入的操作
char[] cbuf = new char[5];
int len;
while ((len = fr.read(cbuf)) != -1) {
// 方式一:
// 注意这里边界值是len
// for (int i = 0; i < len; i++) {
// System.out.print(cbuf[i]);
// }

// 方式二:
String str = new String(cbuf, 0, len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fr != null) {
// 4.关闭流
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

6.3.2 字符流写入文件

  • Writer:输出字符流

    • void write(int c):写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。

    • void write(char[] cbuf):写入字符数组。

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

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

    • void write(String str,int off,int len):写入字符串的某一部分。

    • void flush():刷新该流的缓冲,则立即将它们写入预期目标。

    • public void close() throws IOException:关闭此输出流并释放与该流关联的所有系统资源

代码示例1

需求:从内存中写出数据到硬盘里的文件里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test2() {
FileWriter fw = null;
try {
File file = new File("hello1.txt");
fw = new FileWriter(file);

// 3.写出操作
fw.write("I have a dream!\n");
fw.write("You need to have a dream!");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
// 4.关闭流
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

注意:

  • 输出的文件对应的file可以不存在
    • 不存在的话会自动创建文件。
    • 如果存在,且流使用的构造器是:FileWriter(file, false)FileWriter(file),则对原有文件进行覆盖
    • 如果存在,且流使用的构造器是:FileWriter(file, true),则对原有文件进行内容追加

代码示例2

需求:读取hello.txt的内容,并写入到hello1.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
public void test3 () throws IOException {
FileReader fr = null;
FileWriter fw = null;
try {
File src = new File("hello.txt");
File dest = new File("hello1.txt");

fr = new FileReader(src);
fw = new FileWriter(dest);

char[] cbuf = new char[5];
// 记录每次读入到数组中的数据个数
int len;
while ((len = fr.read(cbuf)) != -1) {
// 每次写入len个字符
fw.write(cbuf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

注意点

字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文本文件。

6.3.3 字节流读取和写入文件

  • InputStream:输入字节流

    • int read():从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。

    • int read(byte[] b):从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。

    • int read(byte[] b, int off, int len):将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1。

    • public void close() throws IOException:关闭此输入流并释放与该流关联的所有系统资源。

  • OutputStream:输出字节流

    • void write(int b):将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。

    • void write(byte[] b):将 b.length 个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用 write(b, 0, b.length) 的效果完全相同。

    • void write(byte[] b,int off,int len):将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。

    • public void flush()throws IOException:刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。

    • public void close() throws IOException:关闭此输出流并释放与该流关联的所有系统资源。

代码示例

需求:实现对图片的复制操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void test0() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File srcFile = new File("test.png");
File destFile = new File("hello.png");

fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);

// 复制的过程
byte[] buffer = new byte[5];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

6.4 处理流

6.4.1 缓冲流

① 概述

为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部缓冲区数组,缺省使用8192个字节(8Kb)的缓冲区。

1
2
3
4
5
6
7
public
class BufferedInputStream extends FilterInputStream {

private static int DEFAULT_BUFFER_SIZE = 8192;
// ...

}

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

  • BufferedInputStreamBufferedOutputStream,处理字节
  • BufferedReaderBufferedWriter,处理字符

注意点

  • 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区。
  • 当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。
  • 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法flush()可以强制将缓冲区的内容全部写入输出流。
  • 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流
  • flush()方法的使用:手动将buffer中内容写入文件。
  • 如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。

image-20220408131609373

② 代码示例

代码示例1

需求:利用缓冲流实现非文本文件的复制操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 实现非文本文件的复制
@Test
public void test0 () {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
File srcFile = new File("test.png");
File destFile = new File("test1.png");

// 造流
// 造节点流
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);

// 造缓冲流
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);

// 复制的过程
byte[] buffer = new byte[10];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源
// 要求:先关闭外层流,再关闭内层流
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

// 说明:外层流关闭时,内层流也会自动关闭,因此可以省略下面两行
// fos.close();
// fis.close();
}

代码示例2

需求:对一个大文件进行复制操作,实现缓冲流和节点流的读写速度对比

代码示例3

需求:利用缓冲流实现文本文件的复制操作

注意简写操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void test1() {
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new FileReader(new File("hello.txt")));
bw = new BufferedWriter(new FileWriter(new File("hello1.txt")));
// 复制过程
char[] buffer = new char[10];
int len;
while ((len = br.read(buffer)) != -1) {
bw.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

6.4.2 转换流

① 概述

转换流提供了在字节流和字符流之间的转换。

Java API提供了两个转换流,两者都属于字符流:

  1. InputStreamReader:将InputStream转换为Reader
  2. OutputStreamWriter:将Writer转换为OutputStream

字节流中的数据都是字符时,转成字符流操作更高效。 很多时候我们使用转换流来处理文乱码问题。

实现编码和解码(字节/字节数组 —> 字符数组/字符串)的功能。

image-20220408142716586

  • InputStreamReader:字符输入流

    • 实现将字节的输入流按指定字符集转换为字符的输入流。
    • 需要和InputStream“套接”。
    • 构造器:
      • public InputStreamReader(InputStream in)
      • public InputSreamReader(InputStream in, String charsetName)
  • OutputStreamWriter:字符输出流

    • 实现将字符的输出流按指定字符集转换为字节的输出流。
    • 需要和OutputStream“套接”。
    • 构造器:
      • public OutputStreamWriter(OutputStream out)
      • public OutputSreamWriter(OutputStream out, String charsetName)
② 字符集
  • 编码表的由来:计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表。
  • 常见的编码表:
    • ASCII:美国标准信息交换码。用一个字节的7位可以表示。
    • ISO8859-1:拉丁码表。欧洲码表用一个字节的8位表示。
    • GB2312:中国的中文编码表。最多两个字节编码所有字符
    • GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
    • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
    • UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。

image-20220408144654011

面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF- 8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。

Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。

  • ANSI编码,通常指的是平台的默认编码,例如英文操作系统中是ISO-8859-1,中文系统是GBK
  • Unicode字符集只是定义了字符的集合和唯一编号,Unicode编码,则是对UTF-8、UCS-2/UTF-16等具体编码方案的统称而已,并不是具体的编码方案。
③ 代码示例

代码示例1

需求:用字节流(通过字符转换流转换)读取一个txt文件,将读取的内容输出到控制台上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test0() throws IOException {
FileInputStream fis = new FileInputStream(new File("hello.txt"));
// 具体使用那个字符集,取决于要读取的文件保存时使用的字符集
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
char[] cbuf = new char[20];
int len;
while ((len = isr.read(cbuf)) != -1) {
String str = new String(cbuf, 0, len);
System.out.println(str);
}

isr.close();
}
1
2
hello world你好世界 // UTF-8
hello world浣犲ソ涓栫晫 // gbk

代码示例2

需求:用字符转换流复制文本文件,并更换编码方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test1() throws IOException {
// 造流
InputStreamReader isr = new InputStreamReader(
new FileInputStream(new File("hello.txt")),
StandardCharsets.UTF_8);
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(new File("hello_gbk.txt")),
"gbk");

// 复制过程
char[] cbuf = new char[20];
int len;
while ((len = isr.read(cbuf)) != -1) {
osw.write(cbuf, 0, len);
}

// 释放资源
isr.close();
osw.close();
}

6.4.3 标准输入输出流

  • System.inSystem.out分别代表了系统标准的输入和输出设备

    • 默认输入设备是:键盘,输出设备是:显示器
    • System.in的类型是InputStream,字节输入流

    • System.out的类型是PrintStream,字节输出流,其是OutputStream的子类FilterOutputStream 的子类

  • 重定向:通过System类的setIn,setOut方法对默认设备进行改变。

    • public static void setIn(InputStream in)
    • public static void setOut(PrintStream out)

代码示例

需求:从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序。

思路:System.in(字节输入流) --> 转换流 --> BufferedReader的readLine()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) {
BufferedReader br = null;
try {
InputStreamReader isr = new InputStreamReader(System.in);
br = new BufferedReader(isr);
while (true) {
System.out.println("请输入字符串: ");
String data = br.readLine();
if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
System.out.println("程序结束");
break;
}
String upperCase = data.toUpperCase();
System.out.println(upperCase);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

6.4.4 打印流

6.4.5 数据流

6.4.6 对象流

① 概述

ObjectInputStreamOjbectOutputSteam:用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。

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

反序列化:用ObjectInputStream类读取基本类型数据或对象的机制

ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量

对象的序列化

  • 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
  • 序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。
  • 序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是JavaEE 平台的基础。
  • 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotSerializableException异常

    • Serializable
    • Externalizable
  • 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:

    • private static final long serialVersionUID;
    • serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。
    • 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议显式声明。
  • 简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)
② 代码示例

代码示例1

需求:

  • 序列化:将内存中的java对象保存到磁盘中
  • 反序列化:将磁盘中的Java对象读入到内存中
  • 注意写出一次,操作flush()一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 序列化
@Test
public void test0() {
ObjectOutputStream oos = null; // 字节流
try {
oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(new String("我爱北京天安门")); // String实现了序列化接口
oos.flush(); // 刷新
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

// 反序列化
@Test
public void test1() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("object.txt"));
Object o = ois.readObject();
String str = (String) o;
System.out.println(str); // 我爱北京天安门
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

代码示例2

需求:自定义类的序列化和反序列化

  • Person类:
1
2
3
4
5
6
7
8
9
@Data
@ToString
@AllArgsConstructor
public class Person implements Serializable { // 实现序列化接口
// 自定义序列化版本标识符
public static final long serialVersionUID = 12345678910L;
private String name;
private int age;
}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Test
public void test0() {
ObjectOutputStream oos = null; // 字节流
try {
oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
oos.writeObject(new String("我爱北京天安门"));
oos.flush();

oos.writeObject(new Person("Mark", 24));
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

@Test
public void test1() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("object.dat"));
Object o = ois.readObject();
String str = (String) o;
System.out.println(str);

Person p = (Person) ois.readObject();
System.out.println(p);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

结果:

1
2
我爱北京天安门
Person(name=Mark, age=24)

注意:如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field的类也不能序列化。

6.4.7 随机存取文件流

① 概述
  • RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并且它实现了DataInput、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读写。如果文件不存在则会去创建文件,如果存在则不会创建,且默认情况下对原内容进行覆盖操作。
  • 我们可以用RandomAccessFile这个类,来实现一个多线程断点下载的功能,用过下载工具的朋友们都知道,下载前都会建立两个临时文件,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以自己实现下。
② 代码示例

代码示例1

需求:复制图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Test
public void test0() {
RandomAccessFile raf1 = null;
RandomAccessFile raf2 = null;
try {
// 字节输入流
raf1 = new RandomAccessFile(new File("test.png"), "r");
// 字节输入输出流,作为输出流使用
raf2 = new RandomAccessFile(new File("test2.png"), "rw");
byte[] buffer = new byte[1024];
int len;
while ((len = raf1.read(buffer)) != -1) {
raf2.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (raf2 != null) {
try {
raf2.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (raf1 != null) {
try {
raf1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

代码示例2

需求:给定一个字符文件,内容为abcdefghijklmn,要求在abc后插入(且不覆盖后面的字符)字符xyz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test1() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");
// 将指针指向第三个位置,即第四个字符d
raf1.seek(3);
// 保存指针后的所有数据到sb中
StringBuilder sb = new StringBuilder((int) new File("hello.txt").length());
byte[] buffer = new byte[1024];
int len;
while ((len = raf1.read(buffer)) != -1) {
sb.append(new String(buffer, 0, len));
}
// 调回指针
raf1.seek(3);
raf1.write("xyz".getBytes());
// 将sb的数据写入到文件
raf1.write(sb.toString().getBytes());
raf1.close();
}

6.5 NIO

略,详见实用技术-Netty学习笔记

7 网络编程

略,详见SpringMVC学习笔记Java Web学习笔记

8 反射机制

8.0 Java程序编译和运行过程

8.0.1 流程和JVM内存模型

  • 编辑源代码(.java
  • 将源代码.java编译字节码文件.class
  • JVM对字节码文件进行解析执行(加载进内存执行),输出结果。

image-20230620172119572

在jvm中,通过类加载器(ClassLoader)将字节码文件(.class)加载进内存,java解释器将字节码翻译成对应的机器码,最后在操作系统解释运行。

线程共享

  • 方法区

主要包括:常量、静态变量、类信息(Class类型的对象)、运行时常量池,操作的是直接内存。各个线程共享。

实际上,方法区在物理上是堆的一部分,但是《Java虚拟机规范》在实现上更支持将方法区从堆逻辑中划分出来成为一块独立的内存区。

  • 堆区

存储该程序在运行时所有创建的对象(或称为实例)。一个JVM实例只存在一个堆内存,所有线程共享,包含新生区、养老区、永久区。

线程独占

  • 虚拟机栈

每个线程都在这个空间有一个私有的空间。线程栈由多个栈帧Stack Frame组成。栈帧内容包含:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。

  • 本地方法栈

和虚拟机栈功能类似,虚拟机栈是为虚拟机执行JAVA方法而准备的,本地方法栈是为虚拟机使用Native本地方法而准备的。

  • 程序计数器

记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行Native方法,则计数器值为空。

8.0.2 相关命令

  • javac

javacjava compile)命令用于将Java源代码编译成Java字节码文件(.class文件),以便在Java虚拟机(JVM)上运行。

语法格式:

1
javac [options] source_file(s)
  • java

java命令用于启动Java虚拟机并执行Java程序。使用java命令可以在命令行中直接运行编译后的Java程序。

语法格式:

1
java [options] class [args...]
  • options:Java命令提供了多个选项来控制Java虚拟机和应用程序的行为。可以使用java -help查看所有选项及其说明。
  • class:要运行的Java类名。
  • args…:传递给主方法的参数。这些参数将作为字符串数组传递给main()方法。

示例

  • 新建Test.java,注意文件名必须和类名相同!
1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
System.out.println("Hello World!");
for(String s : args) {
System.out.println(s);
}
}
}
  • 编译
1
javac test.java

image-20230620172910523

生成了字节码文件Test.class

  • 运行
1
java Test I Love Java!

image-20230620173029533

8.1 反射机制概述

8.1.1 概述

  • Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

  • 加载完类之后,在JVM内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。

    image-20211215224638775

8.1.2 与反射有关的API

  • java.lang.Class:代表一个类

  • java.lang.reflect.Method:代表类的方法

  • java.lang.reflect.Field:代表类的成员变量

  • java.lang.reflect.Constructor:代表类的构造器

8.1.3 利用反射之前对Person类的操作

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Person {
private String name;// 私有属性
public int age;// 公共属性

// 公共方法
public void show(){
System.out.println("我是一个人");
}

// 私有方法
private String showNation(String nation){
System.out.println("我的国籍是"+nation);
return nation;
}

// 私有构造器
private Person(String name) {
this.name = name;
}

// 公共构造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 省略get,set,toString方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 反射之前对于Person类的操作
@Test
public void test(){
// 1.创建Person类的对象
Person p1 = new Person("Tom",22);

// 2. 通过对象,调用其内部的属性和方法
p1.setAge(24);
System.out.println(p1);
p1.show();

// 在Person类外部,不可以通过对象调用其内部私有结构
// 例如:name属性,showNation私有方法,私有构造器
}

8.1.4 利用反射对Person类操作

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 利用反射对Person类的操作
@Test
public void test1() throws Exception{
Class clazz = Person.class;
// 1.通过反射创建Person类的对象
Constructor cons = clazz.getConstructor(String.class, int.class);
Object obj = cons.newInstance("Tom", 23);
Person p = (Person)obj;
System.out.println(p);
// 2.通过反射调用对象指定的属性和方法
// 调用属性
Field age = clazz.getDeclaredField("age");
age.set(p,10);
System.out.println(p);
// 调用方法
Method show = clazz.getDeclaredMethod("show");
show.invoke(p);

// 通过反射,可以调用Person类的私有结构,例如私有属性,方法,构造器等
// 调用私有构造器
Constructor cons1 = clazz.getDeclaredConstructor(String.class);
cons1.setAccessible(true);
Person p1 = (Person) cons1.newInstance("Jerry");
System.out.println(p1);

//调用私有属性
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);
name.set(p1,"HanMeimei");
System.out.println(p1);

// 调用私有方法
Method showNation = clazz.getDeclaredMethod("showNation", String.class);
showNation.setAccessible(true);
String nation = (String)showNation.invoke(p1,"中国");// 相当于String nation = p1.showNation("中国");
System.out.println(nation);
}

8.1.5 如何看待反射和封装性

  • 通过直接new对象,或反射的方式都可以调用公共的结构,那么开发中到底用哪个?

回答:建议直接用new的方法。

  • 反射机制与面向对象的封装性是不是矛盾的?

回答:不矛盾。

8.2 理解Class类并获取Class实例

8.2.1 Class类的理解

  • 类的加载过程:

程序在javac.exe命令(编译)后,会生成一个或多个字节码文件(.class结尾),接着使用java.exe命令(运行)对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,此过程就称为==类的加载==。加载到内存中的类,就成为==运行时类==,此运行时类就作为Class类的一个实例对象

  • 换句话说,Class的实例就对应一个运行时类

  • 加载到内存中的运行时类,会缓存一定时间,在此时间之内,我们可以利用不同的方式来获取此运行时类。该运行时类在内存中只有一个。

image-20211215224649657

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class c1 = Object.class; // c1:Object类的实例对象(运行时类)
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.2 获取Class实例的四种方法

  • 调用运行时类的属性:.class
  • 通过运行时类的对象,调用getClass()方法
  • 调用Class的静态方法:forName(String classPath)用的最多
  • 使用类加载器:ClassLoader 了解,用得少

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取Class实例的四种方式
@Test
public void test2() throws ClassNotFoundException {
// 方式1:调用运行时类的属性:.class
Class<Person> clazz = Person.class;
System.out.println(clazz);// class com.hongyi.Person

// 方式2:通过运行时类的对象,调用getClass()方法
Person p1 = new Person();
Class clazz2 = p1.getClass();
System.out.println(clazz2);

// 方式3:调用Class的静态方法:forName(String classPath) 用的最多
// classPath:全类名
Class clazz3 = Class.forName("com.hongyi.Person");
System.out.println(clazz3);

System.out.println(clazz == clazz2);// true
System.out.println(clazz2 == clazz3);// true

// 方式4:使用类的加载器:ClassLoader 了解,用得少
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
Class clazz4 = classLoader.loadClass("com.hongyi.Person");
System.out.println(clazz4);
System.out.println(clazz4 == clazz);// true
}

8.2.3 Class类常用方法

image-20230620175835766

8.3 类的加载与ClassLoader的理解

8.3.1 类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:

image-20211215224659087

  • 加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。

  • 链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。

  1. 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题。

  2. 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。

  3. 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

  • 初始化:
  1. 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。

  2. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

  3. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步

8.3.2 ClassLoader的理解

类加载器

image-20211215224709444

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

类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

ClassLoader

image-20211215224719025

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test3(){
// 获取类加载器,自定义类的加载器类型为系统类加载器
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
System.out.println(classLoader);// jdk.internal.loader.ClassLoaders$AppClassLoader@78308db1

// 获取上一层的类加载器,为扩展类加载器
ClassLoader classLoader1 = classLoader.getParent();
System.out.println(classLoader1);// jdk.internal.loader.ClassLoaders$PlatformClassLoader@c818063

// 获取上一层的类加载器,为引导类加载器,但是获取不了,返回为null
// 引导类加载器主要负责加载核心类库,无法加载自定义类
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);// null

ClassLoader classLoader3 = String.class.getClassLoader();//引导类加载器
System.out.println(classLoader3);// null:获取不了
}

使用ClassLoader加载配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 读取配置文件
@Test
public void test4() throws Exception {
Properties prop = new Properties();
// 此时的文件默认在当前的project下
// 方式1
FileInputStream fis = new FileInputStream("jdbc.properties");
prop.load(fis);

// 方式2:使用类加载器
// 配置文件默认识别为在当前project的src下
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("jdbc1.properties");
prop.load(is);

System.out.println(prop.getProperty("user"));
System.out.println(prop.getProperty("password"));
}

8.4 创建运行时类的对象

8.4.1 newInstance()

此方法实质上是调用运行时类的空参构造器,要想此方法正确的创建运行时类的对象,要求:

  • 运行时类必须提供空参构造器
  • 空参构造器的访问权限必须得够,通常设置为public

Spring中要求提供一个空参构造器,便于通过反射来创建运行时类对象,便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器。

代码演示

1
2
3
4
5
6
7
8
9
@Test
public void test5() throws IllegalAccessException, InstantiationException {
// 获取运行时类
Class clazz = Person.class;
// newInstance():调用此方法,创建对应的运行时类的对象
// 实际上还是通过调用Person类的空参构造器来造对象
Person p = (Person) clazz.newInstance();
System.out.println(p);
}

8.4.2 反射的动态性理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 体会反射的动态性
@Test
public void test6(){
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 = "com.hongyi.Person";
break;
}
try {
Object obj = getInstance(classPath);
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 此方法要创建一个指定全类名的对象
* @param classPath 全限定类名
* @return
* @throws Exception
*/
public Object getInstance(String classPath) throws Exception{
Class clazz = Class.forName(classPath);
return clazz.newInstance();
}

8.5 获取运行时类的完整结构

8.5.1 代码准备

1
2
3
4
5
6
7
8
9
10
11
12
public class Creature<T> implements Serializable {
private char gender;
public double weight;

private void breath(){
System.out.println("生物呼吸");
}

public void eat(){
System.out.println("生物进食");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@MyAnnotation(value = "hi")
public class Human extends Creature<String> implements Comparable<String>,MyInterface{

private String name;
int age;
public int id;

public Human(){

}

@MyAnnotation(value = "ABC")
private Human(String name){
this.name = name;
}

Human(String name,int age){
this.name = name;
this.age = age;
}

@MyAnnotation
private String show(String nation){
System.out.println("我的国籍是:"+nation);
return nation;
}

public String display(String interests){
return interests;
}

public void info() {
System.out.println("我是一个人");
}

public int compareTo(String s) {
return 0;
}
}
1
2
3
public interface MyInterface {
void info();
}
1
2
3
4
5
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "hello";
}

image-20211215224729258

8.5.2 获取运行时类的属性结构和内部结构

getFields():获取当前运行时类及其父类中声明为public的属性

getDeclaredFields():获取当前运行时类的所有属性,不包含父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test7(){
Class clazz = Human.class;
// 获取属性结构
// getFields():获取当前运行时类及其父类中声明为public的属性
Field[] fields = clazz.getFields();
for (Field f : fields){
System.out.println(f);
// public int com.hongyi.Human.id
// public double com.hongyi.Creature.weight
}
System.out.println();
// getDeclaredFields():获取当前运行时类的所有属性,不包含父类
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields){
System.out.println(f);
// private java.lang.String com.hongyi.Human.name
// int com.hongyi.Human.age
// public int com.hongyi.Human.id
}
}

getModifiers()

getType()

getName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 权限修饰符 数据类型 变量名
@Test
public void test8(){
Class clazz = Human.class;
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields){
// 1.获取权限修饰符
int modifiers = f.getModifiers();
System.out.print("权限修饰符:"+Modifier.toString(modifiers)+" ");
// 2.数据类型
Class type = f.getType();
System.out.print("数据类型:"+type.getName()+" ");
// 3.变量名
String name = f.getName();
System.out.println("变量名:"+name);
System.out.println();
}
}

image-20211215224738420

8.5.3 获取运行时类的方法结构

1
2
3
4
5
// 返回此Class对象所表示的类或接口的全部方法
public Method[] getDeclaredMethods()

// 返回此Class对象所表示的类或接口的public的方法
public Method[] getMethods()

8.6 调用运行时类的指定结构

8.6.1 调用指定方法

通过反射,调用类中的方法,通过Method类完成。

1
2
3
4
5
// 若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的setAccessible(true)方法,将可访问private的方法。
// obj: 类对象;若原方法若为静态方法,此时形参Object obj可为null
// args: 若原方法形参列表为空,则Object[] args为null
// 返回值: Object 对应原方法的返回值,若原方法无返回值,此时返回null
Object invoke(Object obj, Object... args);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InvokeTest {
public void test(String[] arg){
for (String string : arg) {
System.out.println(string);
}
}

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<InvokeTest> clazz = InvokeTest.class;
String[] s = new String[]{"Hello","World"};
// test: 方法名
// String[].class: test方法的形参类型
Method method = clazz.getMethod("test", String[].class);
method.invoke(new InvokeTest(), (Object) s);
}
}

执行结果:

1
2
3
4
Hello
World

Process finished with exit code 0

8.6.2 调用指定属性

8.7 反射的应用——动态代理

代理模式:使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。

8.7.1 动态代理相关API

Proxy:专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成被代理类。

静态方法:

1
2
3
4
5
6
7
8
// 创建一个动态代理类所对应的Class对象
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces);

// 直接创建一个动态代理对象
// 被代理类的类加载器
// 被代理类实现的全部接口
// 得到InvocationHandler接口的实现类实例
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

8.7.2 动态代理示例

  • 问题1:如何根据加载到内存中的被代理类,动态创建一个代理类及其对象
  • 问题2:当通过代理类的对象调用方法时,如何动态去调用被代理类中的同名方法

顶层接口

1
2
3
4
5
public interface Human {
String getBelief();

void eat(String food);
}

实现类,且是被代理类

1
2
3
4
5
6
7
8
9
10
11
12
// 被代理类
public class SuperMan implements Human{
@Override
public String getBelief() {
return "I believe I can fly!";
}

@Override
public void eat(String food) {
System.out.println("I like eating " + food);
}
}

  • 代理类工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 代理类的工厂
class ProxyFactory {
/**
* 通过被代理类对象 动态获取 代理类对象
* @param obj 被代理类的对象
* @return 代理类对象
*/
public static Object getProxyInstance(Object obj) {
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(obj);
return Proxy.newProxyInstance(
obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
handler
);
}
}

MyInvocationHandler是一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作,解决了问题2。

  • MyInvocationHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyInvocationHandler implements InvocationHandler {
// 被代理对象
private Object obj;
// 初始化
public void bind(Object obj) {
this.obj = obj;
}

/**
* 当通过代理类的对象,调用方法A时,就会动态调用这个方法
* @param proxy 代理类对象
* @param method 要调用的代理类对象的方法
* @param args 方法参数
* @return 方法的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(obj, args);
}

}
  • 测试类
1
2
3
4
5
6
7
8
9
10
public class ProxyTest {
public static void main(String[] args) {
// 被代理对象
SuperMan superMan = new SuperMan();
// 获取代理类对象
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
System.out.println(proxyInstance.getBelief());
proxyInstance.eat("banana");
}
}

执行结果:

1
2
I believe I can fly!
I like eating banana

可以使用lambda表达式替换MyInvocationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 代理类的工厂
class ProxyFactory {
/**
* 通过被代理类对象 动态获取 代理类对象
* @param obj 被代理类的对象
* @return 代理类对象
*/
public static Object getProxyInstance(Object obj) {
return Proxy.newProxyInstance(
obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
// 此处是InvocationHandler的子实现匿名内部类,采用了lambda表达式
(proxy, method, args) -> {
System.out.println("方法增强...");
return method.invoke(obj, args);
}
);
}
}

执行结果:

1
2
I believe I can fly!
I like eating banana