第一章 多线程

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

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

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

    • 运行中的QQ,运行中的MP3播放器
    • 程序是静态的,进程是动态的
    • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  3. 线程(thread):,进程可进一步细化为线程,是一个程序内部的一条执行路径。

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

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

      个人理解为:单核就是一个cpu在一段时间可以处理多个线程,在一个单位的时间只能处理一个事情,它把多个线程按照某种顺序来执行,所以在一段时间内就是处理了多个线程,所以说是假的多线程。
      第三点可以理解为:一个main主线程里面可以有其他的线程,例如main方法里面调用别的方法,当内部这个方法执行完毕后就会有gc() 来回收,当然不可以让main主线程来做这个事情,应为main主线程还有其他的线程要做。在main内部的方法执行的时候可能出现异常,这个异常当然不可让main以及main内部产生这个异常的方法来处理,这时就有一个异常处理的线程来工作,所以一个java.exe 至少会有3个线程工作。

  • 并行与并发

    • 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
    • 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
  • 使用多线程的优点
  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

线程的创建和使用

  1. Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。

  2. Thread类的特性:

    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常 把run()方法的主体称为线程体
    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
  3. Thread类的构造器

    • Thread():创建新的Thread对象
    • Thread(String threadname):创建线程并指定线程实例名
    • Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
    • Thread(Runnable target, String name):创建新的Thread对象
  • API中创建线程的两种方式
    JDK1.5之前创建新执行线程有两种方法:①继承Thread类的方式;②实现Runnable接口的方式;
  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。
    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
    //1. Declare a class to be a subclass of Thread
    class EvenNumber extends Thread {

    int number = 0;

    public EvenNumber(int number) {
    this.number = number;
    }

    // 2. Override the run method of class Thread.
    @Override
    public void run() {
    for (int i = 0; i < number; i++) {
    if (i % 2 == 0) {
    System.out.println(Thread.currentThread().getName() + ":" + i + " is a even number");

    }
    }
    }
    }

    public class ThreadTest01 {

    public static void main(String[] args) {

    // 3.Build an instance of the subclass
    EvenNumber n = new EvenNumber(10);

    // 4. This instance can be allocated and started
    n.start();

    // This code will be executed in the main method
    for (int i = 0; i < n.number; i++) {
    if (i % 2 == 0) {
    System.out.println(Thread.currentThread().getName() + ":" + i + " is a even number");
    }
    }
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    D:\PATH-EN\java-path\bin\java.exe ...
    main:0 is a even number
    Thread-0:0 is a even number
    Thread-0:2 is a even number
    Thread-0:4 is a even number
    Thread-0:6 is a even number
    Thread-0:8 is a even number
    main:2 is a even number
    main:4 is a even number
    main:6 is a even number
    main:8 is a even number

    Process finished with exit code 0
  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
    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
    // 1. Create a thread is to declare a class that implements the Runnable interface.
    class PrimeRun implements Runnable {
    long minPrime;

    PrimeRun(long minPrime) {
    this.minPrime = minPrime;
    }


    // 2.This class then implements the run method.
    @Override
    public void run() {
    // Compute primes larger than minPrime
    }
    }

    public class ThreadTest02 {

    public static void main(String[] args) {
    // 3. Create an instance of PrimeRun class
    PrimeRun p = new PrimeRun(143);

    // When you create a thread put the instance as arguments and start it
    new Thread(p).start();
    new Thread(p).start();
    }
    }
  1. 避免了单继承的局限性
  2. 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
  • Thread类的有关方法
  1. start():启动当前线程;调用当前线程的run()
  2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  3. currentThread():静态方法,返回执行当前代码的线程
  4. getName():获取当前线程的名字
  5. setName():设置当前线程的名字
  6. yield():释放当前cpu的执行权。(通俗讲就是:停止后重新分配)
  7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。(通俗讲就是:插队)
  8. stop():已过时。当执行此方法时,强制结束当前线程。
  9. sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。(通俗讲就是:暂停多少毫秒)
  10. isAlive():判断当前线程是否存活
  • 线程的优先级
    1
    2
    3
    MAX_PRIORITY:10
    MIN _PRIORITY:1
    NORM_PRIORITY:5
  1. 涉及的方法
    getPriority() :返回线程优先值
    setPriority(int newPriority) :改变线程的优先级
  2. 说明
    • 线程创建时继承父线程的优先级
    • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
  • Java中的线程分为两类
  1. 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
  2. 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
  3. Java垃圾回收就是一个典型的守护线程。
  4. 若JVM中都是守护线程,当前JVM将退出。

线程的生命周期

  • JDK中用Thread.State类定义了线程的几种状态
    要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
    • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
    • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
    • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
    • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
    • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

      点击下方图片查看Thread.State源码,Ctrl+F 搜索”public enum State”查看状态

注意:只有处于NEW状态的线程可以调用start()方法,处于其他状态的线程都不可以调用start()方法,否则将触发IllegalThreadStateException异常

线程的同步

通过Java可以创建多个线程,用户在处理多线程问题时,必须注意这样一个问题:当两个或多个线程同时访问同一个变量,并且一个线程需要修改这个变量时,应对这样的问题做出处理,否则可能发生混乱。例如,一个工资管理负责人正在修改雇员的工资表,而一些雇员正在领取工资,如果允许这样做,必然会出现混乱。因此,工资管理负责人正在修改工资表时(包括他喝杯茶休息一会儿(),不允许任何雇员领取工资,也就是说,这些雇员必须等待。

  • Java对于多线程的安全问题提供了专业的解决方式:同步机制
  1. 同步代码块
    1
    2
    3
    synchronized(同步监视器){
    // 需要被同步的代码
    }
  2. synchronized还可以放在方法声明中,表示整个方法为同步方法。
    1
    2
    3
    public synchronized void show (String name){
    ........
    }

    共享数据:多个线程共同操作的变量。
    同步监视器:俗称”锁”。任何一个类的对象,都可以充当锁。
    要求:多个线程必须要共用同一把锁。

模拟火车站售票程序,开启三个窗口售票。

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
class Window extends Thread{

private static int ticket = 100;
@Override
public void run() {

while(true){

if(ticket > 0){
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}

}

}
}


public class WindowTest {

public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}
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
class Window1 implements Runnable{

private int ticket = 100;

@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}


public class WindowTest1 {

public static void main(String[] args) {
Window1 w = new Window1();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

问题:卖票过程中,出现了重票、错票 —> 出现了线程的安全问题
问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他
线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

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
public class Windows01Test {

public static void main(String[] args) {

TicketWindows tw = new TicketWindows();

Thread t1 = new Thread(tw);
Thread t2 = new Thread(tw);
Thread t3 = new Thread(tw);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}


class TicketWindows implements Runnable {

private static int ticket = 100;

@Override
public void run() {

while (true) {
synchronized (this) {
if (ticket > 0) {

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()
+ " 售票票号为:" + ticket);
ticket--;
} else {
break;
}
}

}
}
}
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
public class WindowsTest02 {

public static void main(String[] args) {

TicketWindows02 t1 = new TicketWindows02();
TicketWindows02 t2 = new TicketWindows02();
TicketWindows02 t3 = new TicketWindows02();

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}


class TicketWindows02 extends Thread {

private static int ticket = 100;

@Override
public void run() {

while (true) {

synchronized (TicketWindows02.class) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()
+ " 售票票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
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
public class Windows03Test {

public static void main(String[] args) {

TicketWindows tw = new TicketWindows();

Thread t1 = new Thread(tw);
Thread t2 = new Thread(tw);
Thread t3 = new Thread(tw);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}


class TicketWindows03 implements Runnable {

private static int ticket = 100;

@Override
public void run() {

while (true) {
show();
}
}

private synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()
+ " 售票票号为:" + ticket);
ticket--;
}
}
}
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 WindowsTest04 {

public static void main(String[] args) {

TicketWindows04 t1 = new TicketWindows04();
TicketWindows04 t2 = new TicketWindows04();
TicketWindows04 t3 = new TicketWindows04();

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}


class TicketWindows04 extends Thread {

private static int ticket = 100;

@Override
public void run() {

while (true) {
show();
}
}

private static synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()
+ " 售票票号为:" + ticket);
ticket--;
}
}
}

由于每次执行的结果都不一样,但是从执行结果来看已经解决了可能发生的重票和错票问题

点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
D:\PATH-EN\java-path\bin\java.exe ...
窗口1 售票票号为:100
窗口1 售票票号为:99
窗口1 售票票号为:98
窗口1 售票票号为:97
窗口1 售票票号为:96
.....
.....
.....
.....
窗口3 售票票号为:4
窗口3 售票票号为:3
窗口3 售票票号为:2
窗口3 售票票号为:1

Process finished with exit code 0

针对于线程的同步个人理解:线程同步解决了线程安全的问题,线程安全问题的产生就是可能多个线程同时处理了线程们共用的属性。解决的思路也就是让线程们公用的属性在某一时间只能被一个线程使用。用一个“锁”来形容就是,当某个线程处理公共的属性时,给这个属性添加“锁”,处理完了在打开,所以只有当某个线程处理完了,下一个线程才可以接手。当然刚处理完的线程和其他线程享有平等的处理属性的权力。

针对于上方售票例子个人理解:我们把线程们公用的操作叫做共享数据,同步监视器可以是任意的对象,但有一个要求就是,多个线程公用一个锁即同一个对象,对于实现的方式来说,我们只做了一次的 TicketWindows tw = new TicketWindows(); 操作,所以synchronized(this),就指的是 tw。但是对于继承的方式来说我们做了三次的TicketWindows02 t1 = new TicketWindows02();
TicketWindows02 t2 = new TicketWindows02();TicketWindows02 t3 = new TicketWindows02();操作,每一次new都是一个新的对象。所以此时的 this 就分别是t1,t2,t3,不是同一个对象,所以不可以用 this ,这是就引入了,Class.class,我们目前没有学过这个东西,只需要知道类也是对象。且Class.class只生成一次。另外对于synchronized大括号包裹的范围不可以多包,也不可以少包裹,少包很好解释就是没有包裹的就不会被同步,这对于我们来说是肯定不可以发生的,多包的话有时候很难发现,例如上方1.使用同步代码块处理上方实现产生的线程不安全问题第29行代码while(true),如果第30行在第29行上面。这就会产生问题。运行会发现,一直只是一个窗口在处理票,不会有其他的窗口参与。这其实也很好解释,如果synchronized同步了while(true),就说明,开始时某一个线程抢得执行权,然后一直都是这个线程内部在死循环,直接这个线程内部把票给处理完了,其他得线程根本进不来,所以就产生了这种的现象。


  • 解决单例设计模式懒汉式线程不安全问题
    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
    // 方式一
    public class SingletonTest {

    public static void main(String[] args) {
    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();
    System.out.println(instance1 == instance2);
    }
    }

    class Singleton {

    private Singleton() {
    }

    private static Singleton instance = null;

    public static Singleton getInstance() {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }
    }
    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
    // 方式二
    public class SingletonTest {

    public static void main(String[] args) {
    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();
    System.out.println(instance1 == instance2);
    }
    }

    class Singleton {

    private Singleton() {
    }

    private static Singleton instance = null;

    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    D:\PATH-EN\java-path\bin\java.exe ...
    true

    Process finished with exit code 0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class SingletonTest {

    public static void main(String[] args) {
    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();
    System.out.println(instance1 == instance2);
    }
    }

    class Singleton {

    private Singleton() {
    }

    private static Singleton instance = null;

    public static synchronized Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    D:\PATH-EN\java-path\bin\java.exe ...
    true

    Process finished with exit code 0

    个人理解:上面的同步代码块实现解决懒汉式方式一效率相较方式二稍差些,可以举例:例如很多人都想去商店买商品,如果此时商品只有一个,但很多人都要买,就需要每个人都去商店询问是否还有商品,如果此时在门口放个牌子说明东西售罄,则后来者就不需要每个人都进店询问了。方式二就是类似,如果new过一次实例则instance一定 != null,故里面的就不需要每次都new都分配内存空间。如果某个线程第一次判断instance==null,就需要进去new,则方式二对于后来的线程则可以不用进去new。


  • 同步机制中的锁

    • 同步锁机制:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。
    • synchronized的锁是什么?
    1. 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
    2. 同步方法的锁:静态方法(类名.class)、非静态方法(this)
    3. 同步代码块:自己指定,很多时候也是指定为this或类名.class
    • 注意:
    1. 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
    2. 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方 法共用同一把锁(this),同步代码块(指定需谨慎)
  • 同步的范围

  1. 如何找问题,即代码是否存在线程安全?(非常重要)
    明确哪些代码是多线程运行的代码
    明确多个线程是否有共享数据
    明确多线程运行代码中是否有多条语句操作共享数据
  2. 如何解决呢?(非常重要)
    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其 他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中
  3. 切记:
    范围太小:没锁住所有有安全问题的代码
    范围太大:没发挥多线程的功能。
  • 释放锁的操作
  1. 当前线程的同步方法、同步代码块执行结束。
  2. 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。
  • 不会释放锁的操作
  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程

  • 线程的死锁问题

    • 死锁:

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

      • 专门的算法、原则
      • 尽量减少同步资源的定义
      • 尽量避免嵌套同步

        死锁的示例

        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 DeadLockTest {

        public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread() {

        @Override
        public void run() {

        synchronized (s1) {
        s1.append("a");
        s2.append("1");

        try {
        Thread.sleep(100);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }

        synchronized (s2) {
        s1.append("b");
        s2.append("2");

        System.out.println(s1);
        System.out.println(s2);
        }
        }
        }
        }.start();


        new Thread(new Runnable() {

        @Override
        public void run() {
        synchronized (s2) {
        s1.append("c");
        s2.append("3");

        try {
        Thread.sleep(100);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }

        synchronized (s1) {
        s1.append("d");
        s2.append("4");

        System.out.println(s1);
        System.out.println(s2);
        }
        }
        }
        }).start();
        }
        }
        点击查看运行结果
        1
        2
        D:\PATH-EN\java-path\bin\java.exe ...


    • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
    • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象
    • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁

      使用锁来解决线程安全问题

      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
      public class LockTest {

      public static void main(String[] args) {
      Lock lock = new Lock();

      Thread thread1 = new Thread(lock);
      Thread thread2 = new Thread(lock);
      Thread thread3 = new Thread(lock);

      thread1.setName("线程一");
      thread2.setName("线程二");
      thread3.setName("线程三");

      thread1.start();
      thread2.start();
      thread3.start();
      }
      }

      class Lock implements Runnable {

      private ReentrantLock lock = new ReentrantLock();

      private int num = 200;

      @Override
      public void run() {
      while (true) {
      try {
      lock.lock();

      if (num > 0) {

      try {
      Thread.sleep(100);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }

      System.out.println(Thread.currentThread().getName() + ":" + num);
      num--;
      } else {
      break;
      }
      } finally {
      lock.unlock();
      }
      }
      }
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      D:\PATH-EN\java-path\bin\java.exe ...
      线程二:200
      线程二:199
      线程二:198
      线程二:197
      .......
      .......
      .......
      线程一:5
      线程一:4
      线程一:3
      线程一:2
      线程一:1

      Process finished with exit code 0
  • synchronized与Lock的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

线程间的通信问题

问题引入:使用两个线程打印 1-100。线程1, 线程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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class CommunicationTest {

public static void main(String[] args) {

Communication communication = new Communication();

Thread t1 = new Thread(communication);
Thread t2 = new Thread(communication);

t1.setName("线程一:");
t2.setName("线程二:");

t1.start();
t2.start();
}
}

class Communication implements Runnable {

private int number = 0;

@Override
public void run() {

while (true) {

synchronized (this) {

notify();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

if (number < 100) {
number++;
System.out.println(Thread.currentThread().getName() + number);

try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}

} else {
break;
}
}
}
}
}
点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
D:\PATH-EN\java-path\bin\java.exe ...
线程一:1
线程二:2
线程一:3
线程二:4
线程一:5
线程二:6
线程一:7
线程二:8
线程一:9
.....
.....
线程二:94
线程一:95
线程二:96
线程一:97
线程二:98
线程一:99
线程二:100

Process finished with exit code 0

这样理解:刚开始两个线程某个抢得执行权,进来后查看没有wait的线程,所以执行下面代码.当执行到wait()后这个线程等待,释放锁,则另一个线程,也只有另外这个线程获得执行权.这个线程进来后发现有一个线程在wait,就执行notify代码,前一个线程就被释放了.这时前一个线程进入RUNABLE状态等待重新分配.此时已经进入的线程就执行run.到达wait后等待,且释放锁,这时被在RUNABLE的线程捕获进入,依次循环

  • wait() 与 notify() 和 notifyAll()

    • wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
    • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
    • notifyAll ():唤醒正在排队等待资源的所有线程结束等待.

      注意:这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

  • wait()方法特点

  1. 在当前线程中调用方法: 对象名.wait()
  2. 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify(或notifyAll) 为止。
  3. 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  4. 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
  5. 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。
  • notify()/notifyAll()特点
  1. 在当前线程中调用方法: 对象名.notify()
  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
    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    public class ProductTest {

    public static void main(String[] args) {
    Clerk clerk = new Clerk();

    Producer p1 = new Producer(clerk);
    p1.setName("生产者一:");

    Consumer c1 = new Consumer(clerk);
    c1.setName("消费者一:");

    p1.start();
    c1.start();
    }
    }


    class Clerk {

    private int productCount = 0;

    public synchronized void produceProduct() {
    if (productCount < 20) {
    productCount++;
    System.out.println(Thread.currentThread().getName()
    + "开始生产第:" + productCount + "个产品");
    notify();
    } else {
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    public synchronized void consumeProduct() {
    if (productCount > 0) {
    System.out.println(Thread.currentThread().getName()
    + "开始消费第:" + productCount + "个产品");
    productCount--;
    notify();
    } else {
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    class Producer extends Thread {

    private Clerk clerk;

    public Producer(Clerk clerk) {
    this.clerk = clerk;
    }

    @Override
    public void run() {
    System.out.println(Thread.currentThread().getName()
    + "开始生产....");
    while (true) {

    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    clerk.produceProduct();
    }
    }
    }

    class Consumer extends Thread {

    private Clerk clerk;

    public Consumer(Clerk clerk) {
    this.clerk = clerk;
    }

    @Override
    public void run() {
    System.out.println(Thread.currentThread().getName()
    + "开始消费....");
    while (true) {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    clerk.consumeProduct();
    }
    }
    }
    点击查看运行结果
    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
    D:\PATH-EN\java-path\bin\java.exe ...
    生产者一:开始生产....
    消费者一:开始消费....
    生产者一:开始生产第:1个产品
    消费者一:开始消费第:1个产品
    生产者一:开始生产第:1个产品
    生产者一:开始生产第:2个产品
    消费者一:开始消费第:2个产品
    生产者一:开始生产第:2个产品
    生产者一:开始生产第:3个产品
    消费者一:开始消费第:3个产品
    生产者一:开始生产第:3个产品
    消费者一:开始消费第:3个产品
    生产者一:开始生产第:3个产品
    .......
    .......
    生产者一:开始生产第:20个产品
    消费者一:开始消费第:20个产品
    生产者一:开始生产第:20个产品
    消费者一:开始消费第:20个产品
    生产者一:开始生产第:20个产品
    消费者一:开始消费第:20个产品
    生产者一:开始生产第:20个产品
    消费者一:开始消费第:20个产品
    生产者一:开始生产第:20个产品

    Process finished with exit code 130

JDK5.0新增线程创建方式

  • 新增方式一:实现Callable接口

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

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

      • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是 否完成、获取结果等。
      • FutrueTask是Futrue接口的唯一的实现类
      • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
    • 创建的步骤:

    1. 创建一个实现Callable的实现类
    2. 实现call方法,将此线程需要执行的操作声明在call()中
    3. 创建Callable接口实现类的对象
    4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
    5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
    6. 获取Callable中call方法的返回值get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
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
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadCallable {

public static void main(String[] args) {
// 3.创建Callable接口实现类的对象
SumOfEvenNumber sumOfEvenNumber = new SumOfEvenNumber();

// 4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(sumOfEvenNumber);

// 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();

try {
// 6.获取Callable中call方法的返回值get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
System.out.println("sum=" + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

// 1.创建一个实现Callable的实现类
class SumOfEvenNumber implements Callable {

// 2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {

int sum = 0;
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
点击查看运行结果
1
2
3
4
5
6
7
8
9
D:\PATH-EN\java-path\bin\java.exe ...
0
2
4
6
8
sum=20

Process finished with exit code 0

  • 新增方式二:使用线程池
    背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
    思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。
    好处:

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

  • JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • Futuresubmit(Callabletask):执行任务,有返回值,一般又来执行Callable
    • void shutdown() :关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
  • 步骤

  1. 提供指定线程数量的线程池
    设置线程池的属性
  2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
  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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class ThreadPoolTest {

    public static void main(String[] args) {

    // 1. 提供指定线程数量的线程池
    ExecutorService service = Executors.newFixedThreadPool(10);

    // 2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
    service.execute(new EvenNum());
    service.execute(new OddNumber());

    // 3. 关闭连接池
    service.shutdown();
    // service.submit();

    }
    }

    class EvenNum implements Runnable {

    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    if (i % 2 == 0) {
    System.out.println(Thread.currentThread().getName() +
    ":" + i);
    }
    }
    }
    }

    class OddNumber implements Runnable {

    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    if (i % 2 != 0) {
    System.out.println(Thread.currentThread().getName() +
    ":" + i);
    }
    }
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    D:\PATH-EN\java-path\bin\java.exe ...
    pool-1-thread-1:0
    pool-1-thread-1:2
    pool-1-thread-1:4
    pool-1-thread-1:6
    pool-1-thread-1:8
    pool-1-thread-2:1
    pool-1-thread-2:3
    pool-1-thread-2:5
    pool-1-thread-2:7
    pool-1-thread-2:9

    Process finished with exit code 0

第二章 Java常用类

字符串相关的类

  • String的特性

    • String类:代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
    • String是一个final类,代表不可变的字符序列。
    • 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。
    • String对象的字符内容是存储在一个字符数组value[]中的。
  • String str1 = “abc”;与String str2 = new String(“abc”);的区别?

  1. 字符串常量存储在 字符串常量池,目的是共享
  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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import org.testng.annotations.Test;

public class StringTest {

@Test
public void test01() {
System.out.println("*********************test01 start*********************");
String string01 = "Jermyn";
String string02 = "Jermyn";
System.out.println(string01 == string02);

string01 = "JERMYN";
System.out.println(string01 == string02);

String string03 = "www." + string02 + ".cn";
System.out.println(string03);

String string04 = string03.replace('J', 'j');
System.out.println(string04);

System.out.println("**********************test01 end**********************");
}

@Test
public void test02() {

System.out.println("\n*********************test02 start*********************");

String string01 = new String("Jermyn");
String string02 = new String("Jermyn");

String string03 = "Jermyn";
String string04 = "Jermyn";

System.out.println(string01 == string02);
System.out.println(string03 == string04);

System.out.println(string01 == string03);

System.out.println("**********************test02 end**********************");

}

@Test
public void test03() {
System.out.println("\n*********************test03 start*********************");

String s1 = "Jermyn";
String s2 = ".cn";

String s3 = "Jermyn.cn";
String s4 = "Jermyn" + ".cn";
String s5 = s1 + ".cn";
String s6 = "Jermyn" + s2;
String s7 = s1 + s2;

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

System.out.println(s3 == s6.intern());

System.out.println("**********************test03 end**********************");

}
}

点击查看运行结果
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
D:\PATH-EN\java-path\bin\java.exe ...
*********************test01 start*********************
true
false
www.Jermyn.cn
www.jermyn.cn
**********************test01 end**********************

*********************test02 start*********************
false
true
false
**********************test02 end**********************

*********************test03 start*********************
true
false
false
false
false
false
false
true
**********************test03 end**********************

===============================================
Default Suite
Total tests run: 3, Passes: 3, Failures: 0, Skips: 0
===============================================


Process finished with exit code 0

test01个人理解:字符串有一个特点就是不可变性,所以根据这个特性可以知道,只要时需要变化的字符串都是在方法区的字符串常量池里面重新生成一个新的目的字符串,例如 s1 = “abc” +123,其实不是改变 abc ,而是生成一个新的字符串”abc123”然后新的字符串的地址指向s1。严格按照字符串的不可变性可类比其他的字符串操作。且需要注意的是,字符串常量池是不会存储相同的字符串的。
test02个人理解:我们可以知道,只要new一次就是一个新的对象。且new出来的对象都是存储在堆里面的。那么
像new String(“Jermyn”)的特点就是,一个new的对象,但是括号里面的Jermyn字符串是存储在字符串常量池里面,类比,如果new了两次,显然是两个对象,但是他们的内容都是指向常量池里面的数据。所以可以知道,如果是new了两个相同的对象,他们显然是不相等的,虽然面的数据都是指向常量池里里面的同一个数据。
test03个人理解:只需要记住如果变量参与计算如:s1 += “abc”,此种都是在堆中生成的,相当于new。调用inten()方法返回的值在常量池

字符串相关的方法

  • 字符串相关的类:String常用方法1
  1. int length():返回字符串的长度: return value.length
  2. char charAt(int index): 返回某索引处的字符return value[index]
  3. boolean isEmpty():判断是否是空字符串:return value.length == 0
  4. String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
  5. String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
  6. String trim():返回字符串的副本,忽略前导空白和尾部空白
  7. boolean equals(Object obj):比较字符串的内容是否相同
  8. boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
  9. String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
  10. int compareTo(String anotherString):比较两个字符串的大小
  11. String substring(int beginIndex): 返回一个新的字符串, 它是此字符串的从
  12. beginIndex开始截取到最后的一个子字符串。
  13. String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字 符串从beginIndex开始截取endIndex(不包含)的一个子字符串。
    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97

    import org.testng.annotations.Test;

    import java.util.Locale;

    public class StringMethodTest {

    String string01 = " Jermyn . cn ";
    String string02 = "";


    @Test
    public void test01() {
    // 1. int length():返回字符串的长度return value.length
    System.out.println(string01.length());
    }

    @Test
    public void test02() {
    //2.char charAt(int index): 返回某索引处的字符return value[index]
    System.out.println(string01.charAt(1));
    System.out.println(string01.charAt(11));
    // System.out.println(string01.charAt(13));
    }

    @Test
    public void test03() {
    //3. boolean isEmpty():判断是否是空字符串:return value.length == 0
    System.out.println(string01.isEmpty());
    System.out.println(string02.isEmpty());
    }

    @Test
    public void test04() {
    //4.1 String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
    //4.2 String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
    System.out.println(string01.toLowerCase(Locale.ROOT));
    System.out.println(string01.toUpperCase(Locale.ROOT));
    }

    @Test
    public void test05() {
    // 5.String trim():返回字符串的副本,忽略前导空白和尾部空白
    System.out.println(string01.trim());
    }

    @Test
    public void test06() {
    //6.boolean equals(Object obj):比较字符串的内容是否相同
    System.out.println(string01.equals(string02));
    }

    @Test
    public void test07() {
    // 7.boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
    String s1 = "JERMYN";
    String s2 = "Jermyn";
    System.out.println(s1.equalsIgnoreCase(s2));
    }

    @Test
    public void test08() {
    // 8.String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
    String s1 = "Jermyn";
    String s2 = ".cn";
    System.out.println(s1.concat(s2));
    }

    @Test
    public void test09() {
    // 9.int compareTo(String anotherString):比较两个字符串的大小
    // 负数当前对象小,零对象相等,整数当前对象大
    String s1 = "Jermyn";
    String s2 = "Jermyn_";
    String s3 = "Jermy";

    System.out.println(s1.compareTo(s2));
    System.out.println(s1.compareTo(s3));
    }

    @Test
    public void test10() {
    // 10. String substring(int beginIndex): 返回一个新的字符串, 它是此字符串的从beginIndex开始截取到最后的一个子字符串。
    String s1 = new String("https://www.jermyn.cn");

    System.out.println(s1.substring(8));

    }

    @Test
    public void test11() {
    // 11.String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字 符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.substring(12,18));
    }

    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    D:\PATH-EN\java-path\bin\java.exe ...
    13
    J
    n
    false
    true
    jermyn . cn
    JERMYN . CN
    Jermyn . cn
    false
    true
    Jermyn.cn
    -1
    1
    www.jermyn.cn
    jermyn

    ===============================================
    Default Suite
    Total tests run: 11, Passes: 11, Failures: 0, Skips: 0
    ===============================================


    Process finished with exit code 0
  • 字符串相关的类:String常用方法2
  1. boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
  2. boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
  3. boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
  4. boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列 时,返回 true
  5. int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
  6. int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出 现处的索引,从指定的索引开始
  7. int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
  8. int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后 一次出现处的索引,从指定的索引开始反向搜索注:indexOf和lastIndexOf方法如果未找到都是返回-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
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    import org.testng.annotations.Test;

    public class StringMethodTest02 {

    @Test
    public void test01() {

    // 1.boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.endsWith(".cn"));
    System.out.println(s1.endsWith(".com"));
    }

    @Test
    public void test02() {

    //2.boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.startsWith("https://"));
    System.out.println(s1.startsWith("http://"));

    }

    @Test
    public void test03() {

    // 3.boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.startsWith("www", 8));
    }

    @Test
    public void test04() {

    // 4.boolean contains(CharSequence s):当且仅当此字符串包含指定的char值序列时,返回true
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.contains("www."));
    System.out.println(s1.contains("Jermyn"));
    }

    @Test
    public void test05() {

    // 5.1 int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.indexOf("www"));
    System.out.println(s1.indexOf("wws"));

    // 5.2 int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
    System.out.println(s1.indexOf("www",9));
    }

    @Test
    public void test06() {

    // 6. int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.lastIndexOf(".cn"));
    }

    @Test
    public void test07() {

    // 7. int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后 一次出现处的索引,从指定的索引开始反向搜索
    //注:indexOf和lastIndexOf方法如果未找到都是返回-1
    String s1 = new String("https://www.jermyn.cn");
    System.out.println(s1.lastIndexOf("www", 12));

    }
    }

    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    D:\PATH-EN\java-path\bin\java.exe ...
    true
    false
    true
    false
    true
    true
    false
    8
    -1
    -1
    18
    8

    ===============================================
    Default Suite
    Total tests run: 7, Passes: 7, Failures: 0, Skips: 0
    ===============================================


    Process finished with exit code 0
  • 字符串相关的类:String常用方法3
  1. String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
  2. String replace(CharSequence target, CharSequence replacement): 使 用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
  3. String replaceAll(String regex, String replacement) : 使用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
  4. String replaceFirst(String regex, String replacement) : 使用给定的replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
  5. boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
  6. String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
  7. String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此 字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    import org.testng.annotations.Test;

    public class StringMethodTest03 {

    @Test
    public void test01() {

    //1. String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用newChar 替换此字符串中出现的所有 oldChar 得到的。
    String s1 = new String("https://jermyn.cn");

    // 所有的 char 都替换
    System.out.println(s1.replace("j", "J"));

    // 1.1 String replace(CharSequence target, CharSequence replacement): 使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
    // 所有的 char[] 都替换
    System.out.println(s1.replace("jermyn", "JERMYN"));
    }

    @Test
    public void test02() {

    // 2. String replaceAll(String regex, String replacement) :使用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
    String s1 = "12hello34world5java7891mysql456";

    // 其实正则的表达大同小异,此处不在详细说明,如果需要的话可以查看博客《shell学习笔记》第14,15章
    // https://www.jermyn.cn/posts/d746.html 会有一定启发
    String s2 = s1.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
    System.out.println(s2);
    }

    @Test
    public void test03() {

    // 3. boolean matches(String regex):告知此字符串是否匹配给定的正则表达式
    String str = "12345";
    //判断str字符串中是否全部有数字组成,即有1-n个数字组成 boolean matches = str.matches("\\d+"); System.out.println(matches);
    String tel = "0571-4534289";
    //判断这是否是一个杭州的固定电话
    boolean result = tel.matches("0571-\\d{7,8}");
    System.out.println(result);


    }

    @Test
    public void test04() {

    // 4.String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
    String str = "hello|world|java";
    String[] strs = str.split("\\|");
    for (int i = 0; i < strs.length; i++) {
    System.out.println(strs[i]);
    }
    System.out.println();

    }

    @Test
    public void test05() {

    // 5. String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此 字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中
    String str2 = "hello.world.java";
    String[] strs2 = str2.split("\\.");
    for (int i = 0; i < strs2.length; i++) {
    System.out.println(strs2[i]);
    }

    }

    }

    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    D:\PATH-EN\java-path\bin\java.exe ...
    https://Jermyn.cn
    https://JERMYN.cn
    hello,world,java,mysql
    true
    hello
    world
    java

    hello
    world
    java

    ===============================================
    Default Suite
    Total tests run: 9, Passes: 9, Failures: 0, Skips: 0
    ===============================================


    Process finished with exit code 0

  • 字符串相关的类:String与基本数据类型转换

    • 字符串—>基本数据类型、包装类

      • Integer包装类的public static int parseInt(String s):可以将由“数字”字 符组成的字符串转换为整型。
      • 类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型
    • 基本数据类型、包装类—>字符串

      • 调用String类的public String valueOf(int n)可将int型转换为字符串
      • 相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(doubled)、valueOf(boolean b)可由参数的相应类型到字符串的转换

        示例

        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
        import org.testng.annotations.Test;

        public class StringTest {

        @Test
        public void test01() {

        // String --> char[]调用String的toCharArray()

        String str = "Jermyn.cn";

        char[] chars = str.toCharArray();

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

        // char[] --> String 调用String的构造器
        char[] arr = new char[]{'j', 'e', 'r', 'm', 'y', 'n'};
        String s = new String(arr);
        System.out.println(s);

        }
        }

        点击查看运行结果
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        D:\PATH-EN\java-path\bin\java.exe ...
        J
        e
        r
        m
        y
        n
        .
        c
        n
        jermyn

        ===============================================
        Default Suite
        Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
        ===============================================


        Process finished with exit code 0

  • 字符串相关的类:String与字节数组转换

    • 字节数组—>字符串

      • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
      • String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
    • 字符串—>字节数组

      • public byte[] getBytes() :使用平台的默认字符集将此 String 编码为byte 序列,并将结果存储到一个新的 byte 数组中。
      • public byte[] getBytes(String charsetName) :使用指定的字符集将 此 String 编码到 byte 序列,并将结果存储到新的 byte 数组
        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
        import org.testng.annotations.Test;
        import java.io.UnsupportedEncodingException;
        import java.util.Arrays;

        public class StringTest {

        @Test
        public void test01() throws UnsupportedEncodingException {

        // String --> byte[]
        String str = new String("域名:Jermyn");

        // 使用默认的字符集进行编码,跟自己的设置有关,这个是UTF-8
        byte[] bytes = str.getBytes();
        System.out.println(Arrays.toString(bytes));

        // 指定使用 GBK 进行编码
        byte[] gbks = str.getBytes("GBK");
        System.out.println(Arrays.toString(gbks));

        // byte[]-->String
        // 编解码的字符类型一致
        String s = new String(gbks, "GBK");
        String s1 = new String(bytes, "UTF-8");
        System.out.println(s);
        System.out.println(s1);
        }
        }
        点击查看运行结果
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        D:\PATH-EN\java-path\bin\java.exe ...
        [-27, -97, -97, -27, -112, -115, -17, -68, -102, 74, 101, 114, 109, 121, 110]
        [-45, -14, -61, -5, -93, -70, 74, 101, 114, 109, 121, 110]
        域名:Jermyn
        域名:Jermyn

        ===============================================
        Default Suite
        Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
        ===============================================


        Process finished with exit code 0

  • 字符串相关的类:StringBuffer

    • java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象
    • 很多方法与String相同。
    • 作为参数传递时,方法内部可以改变值(可变)。
    • StringBuffer类不同于String,其对象必须使用构造器生成。有三个构造器:
      • StringBuffer():初始容量为16的字符串缓冲区
      • StringBuffer(int size):构造指定容量的字符串缓冲区
      • StringBuffer(String str):将内容初始化为指定字符串内容
  • 字符串相关的类:StringBuilder
    StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且 提供相关功能的方法也一样

  • StringBuffer类的常用方法

  1. StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
  2. StringBuffer delete(int start,int end):删除指定位置的内容
  3. StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
  4. StringBuffer insert(int offset, xxx):在指定位置插入xxx StringBuffer reverse() :把当前字符序列逆转
  5. 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
    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
    61
    62
    63
    64
    65
    66
    67
    68
    import org.testng.annotations.Test;

    public class StringBufferAndBuilderTest {

    @Test
    public void test01() {

    // 1.StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
    StringBuffer stringBuffer = new StringBuffer("Jermyn");
    StringBuffer stringBuffer01 = stringBuffer.append(".cn");
    System.out.println(stringBuffer01);
    }

    @Test
    public void test02() {

    // 2. StringBuffer delete(int start,int end):删除指定位置的内容
    StringBuffer stringBuffer02 = new StringBuffer("https://jermyn.cn");
    StringBuffer stringBuffer03 = stringBuffer02.delete(0, 8);
    System.out.println(stringBuffer03);
    System.out.println(stringBuffer02);
    }

    @Test
    public void test03() {

    // 3.StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
    StringBuffer stringBuffer04 = new StringBuffer("https://jermyn.cn");
    StringBuffer stringBuffer05 = stringBuffer04.replace(8, 14, "JERMYN");
    System.out.println(stringBuffer05);
    System.out.println(stringBuffer04);
    }

    @Test
    public void test04() {

    // 4.StringBuffer insert(int offset, xxx):在指定位置插入xxx
    StringBuffer stringBuffer = new StringBuffer("http://jermyn.cn");
    System.out.println(stringBuffer);

    StringBuffer stringBuffer01 = stringBuffer.insert(4, "s");
    System.out.println(stringBuffer01);
    }

    @Test
    public void test05() {

    // 5.StringBuffer reverse() :把当前字符序列逆转
    StringBuffer stringBuffer = new StringBuffer("https://jermyn.cn");
    System.out.println(stringBuffer.reverse());
    }

    @Test
    public void test06() {

    StringBuffer stringBuffer = new StringBuffer("https://jermyn.cn");
    System.out.println(stringBuffer.indexOf("jer"));

    System.out.println(stringBuffer.substring(8, 14));

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

    System.out.println(stringBuffer.charAt(5));

    stringBuffer.setCharAt(16,'/');
    System.out.println(stringBuffer);
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    D:\PATH-EN\java-path\bin\java.exe ...
    Jermyn.cn
    jermyn.cn
    jermyn.cn
    https://JERMYN.cn
    https://JERMYN.cn
    http://jermyn.cn
    https://jermyn.cn
    nc.nymrej//:sptth
    8
    jermyn
    17
    :
    https://jermyn.c/

    ===============================================
    Default Suite
    Total tests run: 6, Passes: 6, Failures: 0, Skips: 0
    ===============================================


    Process finished with exit code 0

    个人理解:关于String,StringBuffer,StringBuilder,开发中建议选择StringBuffer(int capacity),capacity为数组的长度,后两者较前者是可变的 string ,底层源码显示,他们都是初始定义一个长度为16字符的数组,长度增加时就扩容,每次扩容的长度为16+(value.leng<<1)+2,就是,原来长度的二倍再加2,同时将原来的数组copy到新扩容的数组中,由于有了copy的操作,所以才有了前面说的,选择StringBuffer指定String的长度,这个长度自己预估总共的append的次数,且预估最终的长度,这样就避免了多次扩容时的copy操作导致性能的下降。是否选择StringBuilder就看处理的数据是否需要线程的安全。需要则选择后者。

  • 对比String、StringBuffer、StringBuilder
  1. String(JDK1.0):不可变字符序列
  2. StringBuffer(JDK1.0):可变字符序列、效率低、线程安全
  3. StringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全
    注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder会改变其值。
    三者之间的转化就调用目的类型的构造器。

运行效率测试

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
public class OperationEfficiencyTest {

public static void main(String[] args) {
//初始设置
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
4
5
6
D:\PATH-EN\java-path\bin\java.exe ...
StringBuffer的执行时间:4
StringBuilder的执行时间:3
String的执行时间:1257

Process finished with exit code 0

从高到低排列:StringBuilder > StringBuffer > String

JDK8之前日期时间API

  1. java.lang.System类
    System类提供的public static long currentTimeMillis()用来返回当前时 间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。
  2. java.util.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
        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
        import org.testng.annotations.Test;

        import java.util.Date;

        public class DateTimeTest {

        @Test
        public void test01() {

        long l = System.currentTimeMillis();
        System.out.println(l);
        }

        @Test
        public void test02(){

        Date date = new Date();
        System.out.println(date.toString());

        System.out.println(date.getTime());

        Date date1 = new Date(14564116416L);
        System.out.println(date1.toString());
        System.out.println(date1.getTime());
        }

        @Test
        public void test03(){

        java.sql.Date date = new java.sql.Date(35235325L);
        System.out.println(date);

        //如何将java.util.Date对象转换为java.sql.Date对象
        //情况一:强转
        Date date1 = new java.sql.Date(2343243242323L);
        java.sql.Date date5 = (java.sql.Date) date1;

        //情况二:
        Date date2 = new Date();
        java.sql.Date date3 = new java.sql.Date(date2.getTime());
        System.out.println(date3);
        }
        }
        点击查看运行结果
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        D:\PATH-EN\java-path\bin\java.exe ...
        1665548341186
        Wed Oct 12 12:19:01 CST 2022
        1665548341188
        Thu Jun 18 21:35:16 CST 1970
        14564116416
        1970-01-01
        2022-10-12

        ===============================================
        Default Suite
        Total tests run: 3, Passes: 3, Failures: 0, Skips: 0
        ===============================================


        Process finished with exit code 0

  • 3. java.text.SimpleDateFormat类

    • Date类的API不易于国际化,大部分被废弃了,java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类。
    • 它允许进行格式化:日期—>文本、解析:文本—>日期
    • 格式化:

      • SimpleDateFormat() :默认的模式和语言环境创建对象
      • public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象,该对象调用:
      • 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
        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

        import java.text.ParseException;
        import java.text.SimpleDateFormat;
        import java.util.Date;

        public class DateTimeTest {

        public static void main(String[] args) throws ParseException {

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat();


        // 1.格式化 日期 --> 字符串
        Date date = new Date();

        // date 输出相当于调用了 toString 方法
        System.out.println(date);

        // 默认的格式化输出
        String format = simpleDateFormat.format(date);
        System.out.println(format);

        // 2. 解析 字符串 --> 日期
        String str = "22-10-12 下午5:12";


        // 显然解析的过程,会与格式有关,所以避免报错,必须异常处理。
        Date parse = simpleDateFormat.parse(str);
        System.out.println(parse);

        // // 错误
        // String s = "22-10-12 17:18";
        // Date parse1 = simpleDateFormat.parse(s);
        // System.out.println(parse1);


        //*************按照指定的方式格式化和解析:调用带参的构造器*****************
        SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

        // 格式化
        String format1 = simpleDateFormat1.format(date);
        System.out.println(format1);

        // 解析
        Date parse1 = simpleDateFormat1.parse(format1);
        System.out.println(parse1);

        Date parse2 = simpleDateFormat1.parse("1999-11-15 00:00:00");
        System.out.println(parse2);


        }
        }
        点击查看运行结果
        1
        2
        3
        4
        5
        6
        7
        8
        9
        D:\PATH-EN\java-path\bin\java.exe ...
        Wed Oct 12 17:48:27 CST 2022
        22-10-12 下午5:48
        Wed Oct 12 17:12:00 CST 2022
        2022-10-12 05:48:27
        Wed Oct 12 05:48:27 CST 2022
        Mon Nov 15 00:00:00 CST 1999

        Process finished with exit code 0

        个人理解:格式化过程:①调用SimpleDateFormat的构造器,生成实例,构造器的内容时格式。②调用SimpleDateFormat的format方法,格式化时间date。解析过程:调用parse方法。

习题:字符串”2000-03-05”,转换为java.sql.Date

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateTest {

public static void main(String[] args) throws ParseException {
String birthStr = "2000-03-05";

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date parse = simpleDateFormat.parse(birthStr);

java.sql.Date sqlDate = new java.sql.Date(parse.getTime());
System.out.println(sqlDate);
}
}

点击查看运行结果
1
2
3
4
D:\PATH-EN\java-path\bin\java.exe ...
2000-03-05

Process finished with exit code 0
  • 4. java.util.Calendar(日历)类

    • Calendar是一个抽象基类,主用用于完成日期字段之间相互操作的功能。
    • 获取Calendar实例的方法

      • 使用Calendar.getInstance()方法
      • 调用它的子类GregorianCalendar的构造器。
    • 一个Calendar的实例是系统时间的抽象表示,通过get(int field)方法来取得想 要的时间信息。比如YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、 MINUTE、SECOND

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

        注意:

        • 获取月份时:一月是0,二月是1,以此类推,12月是11
        • 获取星期时:周日是1,周二是2 , 。。。。周六是7
1
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
61
62
63
import org.testng.annotations.Test;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

public class CalenderTest {

// 方法一:Calendar 抽象类可以new,但有 getInstance() 方法
Calendar instanceCal = Calendar.getInstance();

// 方法二:子类GregorianCalendar
GregorianCalendar gregorianCalendar = new GregorianCalendar();

@Test
public void testCalender() {

int days = instanceCal.get(Calendar.DAY_OF_MONTH);
System.out.println("今天是这个月的第:" + days + "天");
System.out.println("今天是这年的第:" + instanceCal.get(Calendar.DAY_OF_YEAR) + "天");
}

@Test
public void testCalender01() {

instanceCal.set(Calendar.DAY_OF_MONTH, 15);

System.out.println("今天是这个月的第:"
+ instanceCal.get(Calendar.DAY_OF_MONTH) + "天");
}

@Test
public void testCalender02() {

System.out.println("今天是这个月的第:"
+ gregorianCalendar.get(Calendar.DAY_OF_MONTH) + "天");

// 今天加5天是这个月的第几天
gregorianCalendar.add(Calendar.DAY_OF_MONTH, 5);

System.out.println("今天是这个月的第:"
+ gregorianCalendar.get(Calendar.DAY_OF_MONTH) + "天");

gregorianCalendar.add(Calendar.DAY_OF_MONTH, -15);

System.out.println("今天是这个月的第:"
+ gregorianCalendar.get(Calendar.DAY_OF_MONTH) + "天");

}

@Test
public void testCalender03() {

Date time = instanceCal.getTime();
System.out.println(time);

instanceCal.setTime(time);

System.out.println("今天是这个月的第:"
+ gregorianCalendar.get(Calendar.DAY_OF_MONTH) + "天");
}
}

点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
D:\PATH-EN\java-path\bin\java.exe ...
今天是这个月的第:12
今天是这年的第:285
今天是这个月的第:15
今天是这个月的第:12
今天是这个月的第:17
今天是这个月的第:2
Sat Oct 15 20:44:16 CST 2022
今天是这个月的第:2

===============================================
Default Suite
Total tests run: 4, Passes: 4, Failures: 0, Skips: 0
===============================================


Process finished with exit code 0

JDK8中新日期时间API

  • 新日期时间API出现的背景
    JDK 1.0中包含了 一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:
    可变性:像日期和时间这样的类应该是不可变的。
    偏移性:Date中的年份是从1900开始的,而月份都从0开始。 格式化:格式化只对Date有用,Calendar则不行。
    此外,它们也不是线程安全的;不能处理闰秒等。

  • 新时间日期API

    • 第三次引入的API是成功的,并且Java 8中引入的java.time API 已经纠正了过去的缺陷,将来很长一段时间内它都会为我们服务。
    • Java 8 吸收了 Joda-Time 的精华,以一个新的开始为Java创建优秀的API。 新的java.time 中包含了所有关于本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类。历史悠久的 Date 类新增了 toInstant() 方法, 用于把 Date 转换成新的表示形式。这些新增的本地化时间日期 API 大大简化了日期时间和本地化的管理。
  • 新时间日期API
    java.time – 包含值对象的基础包
    java.time.chrono – 提供对不同的日历系统的访问
    java.time.format – 格式化和解析时间和日期
    java.time.temporal – 包括底层框架和扩展特性
    java.time.zone – 包含时区支持的类

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

    • LocalDate代表IOS格式(yyyy-MM-dd)的日期,可以存储 生日、纪念日等日期。
    • LocalTime表示一个时间,而不是日期。
    • LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一。
      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
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      import org.testng.annotations.Test;

      import java.time.DayOfWeek;
      import java.time.LocalDate;
      import java.time.LocalDateTime;
      import java.time.LocalTime;
      import java.util.Date;

      public class Jdk8DateTimeTest {

      @Test
      public void test01() {

      Date date1 = new Date(2000, 3, 5);
      System.out.println(date1);

      // 偏移量
      Date date = new Date(2000 - 1900, 3 - 1, 5);
      System.out.println(date);

      }

      @Test
      public void test02() {

      // now()/*now(ZoneId zone):静态方法,根据当前时间创建对象/指定时区的对象。
      LocalDate now1 = LocalDate.now();
      System.out.println(now1);

      LocalTime now2 = LocalTime.now();
      System.out.println(now2);

      LocalDateTime now = LocalDateTime.now();
      System.out.println(now);

      }

      @Test
      public void test03() {

      // of():静态方法,根据指定日期/时间创建对象
      LocalDateTime of = LocalDateTime.of(2000, 3, 5, 0, 0, 0);
      System.out.println(of);

      }

      @Test
      public void test04() {

      // getDayOfMonth()/getDayOfYear():获得月份天数(1-31) /获得年份天数(1-366)
      LocalDateTime now = LocalDateTime.now();

      int dayOfMonth = now.getDayOfMonth();
      DayOfWeek dayOfWeek = now.getDayOfWeek();
      int dayOfYear = now.getDayOfYear();
      int monthValue = now.getMonthValue();

      System.out.println(dayOfMonth);
      System.out.println(dayOfYear);
      System.out.println(dayOfWeek);
      System.out.println(monthValue);
      }

      @Test
      public void test05() {

      //withDayOfMonth()/withDayOfYear()/withMonth()/withYear():将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
      LocalDate now = LocalDate.now();
      System.out.println(now);

      // 不可变性,修改不改变原来的数据
      LocalDate localDate = now.withMonth(5);
      System.out.println(localDate);

      // 设置年份
      LocalDate localDate1 = now.withYear(2000);
      System.out.println(localDate1);
      }

      @Test
      public void test07() {

      // plusDays(), plusWeeks(),plusMonths(),plusYears(),plusHours():
      // 向当前对象添加几天、几周、几个月、几年、几小时
      LocalDateTime now = LocalDateTime.now();
      System.out.println(now);

      LocalDateTime localDateTime = now.plusDays(5);
      System.out.println(localDateTime);

      }

      @Test
      public void test08() {

      // minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours()
      LocalDateTime now = LocalDateTime.now();
      System.out.println(now);

      LocalDateTime localDateTime = now.minusDays(2);
      System.out.println(localDateTime);

      }
      }
      点击查看运行结果
      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
      D:\PATH-EN\java-path\bin\java.exe ...
      Thu Apr 05 00:00:00 CST 3900
      Sun Mar 05 00:00:00 CST 2000
      2022-10-13
      12:32:53.852
      2022-10-13T12:32:53.852
      2000-03-05T00:00
      13
      286
      THURSDAY
      10
      2022-10-13
      2022-05-13
      2000-10-13
      2022-10-13T12:32:53.855
      2022-10-18T12:32:53.855
      2022-10-13T12:32:53.856
      2022-10-11T12:32:53.856

      ===============================================
      Default Suite
      Total tests run: 7, Passes: 7, Failures: 0, Skips: 0
      ===============================================


      Process finished with exit code 0
  • 瞬时:Instant

    • Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。
    • 在处理时间和日期的时候,我们通常会想到年,月,日,时,分,秒。然而,这只是 时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连 续的。在此模型中,时间线中的一个点表示为一个很大的数,这有利于计算机 处理。在UNIX中,这个数从1970年开始,以秒为的单位;同样的,在Java中, 也是从1970年开始,但以毫秒为单位。
    • java.time包通过值类型Instant提供机器视图,不提供处理人类意义上的时间 单位。Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。 概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒 数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级。
    • (1ns =10-9s) 1秒=1000毫秒 =10^6微秒=10^9纳秒
  • 相关方法

    • now():静态方法,返回默认UTC时区的Instant类的对象
    • ofEpochMilli(long epochMilli):静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒 数之后的Instant类的对象
    • atOffset(ZoneOffset offset):结合即时的偏移来创建一个 OffsetDateTime
    • toEpochMilli():返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳
      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
      import java.time.Instant;
      import java.time.OffsetDateTime;
      import java.time.ZoneOffset;

      public class InstantTest {

      public static void main(String[] args) {

      // 静态方法,返回默认UTC时区的Instant类的对象
      Instant now = Instant.now();
      System.out.println(now);

      // 结合即时的偏移来创建一个 OffsetDateTime
      OffsetDateTime offsetDateTime = now.atOffset(ZoneOffset.ofHours(8));
      System.out.println(offsetDateTime);

      // 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
      long l = now.toEpochMilli();
      System.out.println(l);

      // 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒 数之后的Instant类的对象静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒 数之后的Instant类的对象
      Instant instant = Instant.ofEpochMilli(1502643933071L);
      System.out.println(instant);
      }
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      D:\PATH-EN\java-path\bin\java.exe ...
      2022-10-13T05:13:35.378Z
      2022-10-13T13:13:35.378+08:00
      1665638015378
      2017-08-13T17:05:33.071Z

      Process finished with exit code 0
  • 格式化与解析日期或时间
    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
    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    import org.testng.annotations.Test;

    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.time.format.FormatStyle;
    import java.time.temporal.TemporalAccessor;


    public class DateTimeFormatterTest {

    DateTimeFormatter instance = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    DateTimeFormatter instance01 = DateTimeFormatter.ISO_LOCAL_DATE;
    DateTimeFormatter instance02 = DateTimeFormatter.ISO_LOCAL_TIME;

    @Test
    public void test01() {

    // 格式化:日期-->字符串
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now);

    String format = instance.format(now);
    System.out.println(format);

    }

    @Test
    public void test02() {

    // 解析:字符串-->日期
    // 方式一:
    String s = new String("2022-10-13T13:35:33.634");
    TemporalAccessor parse = instance.parse(s);
    System.out.println(parse);

    // 方式二:
    LocalDateTime now = LocalDateTime.now();

    // 本地化相关的格式。如:ofLocalizedDateTime()
    // FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :
    // 适用于LocalDateTime
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
    String format = dateTimeFormatter.format(now);
    System.out.println(format);

    DateTimeFormatter dateTimeFormatter01 = DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG);
    String format1 = dateTimeFormatter01.format(now);
    System.out.println(format1);

    // 本地化相关的格式。如:ofLocalizedDate()
    // FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :
    // 适用于LocalDate
    DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
    String format2 = dateTimeFormatter1.format(now);
    System.out.println(format2);

    }

    @Test
    public void test03() {

    // 方式三:
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");

    // 格式化
    String format = dateTimeFormatter.format(LocalDateTime.now());
    System.out.println(format);

    // 解析
    TemporalAccessor parse = dateTimeFormatter.parse(format);
    System.out.println(parse);
    }

    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    D:\PATH-EN\java-path\bin\java.exe ...
    2022-10-13T16:04:59.066
    2022-10-13T16:04:59.066
    {},ISO resolved to 2022-10-13T13:35:33.634
    下午4:04
    下午040459
    2022-10-13
    2022-10-13 04:04:59
    {NanoOfSecond=0, MilliOfSecond=0, MinuteOfHour=4, SecondOfMinute=59, HourOfAmPm=4, MicroOfSecond=0},ISO resolved to 2022-10-13

    ===============================================
    Default Suite
    Total tests run: 3, Passes: 3, Failures: 0, Skips: 0
    ===============================================


    Process finished with exit code 0

Java比较器

在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。
Java实现对象排序的方式有两种:

  1. 自然排序:java.lang.Comparable
  2. 定制排序:java.util.Comparator
  • 方式一:自然排序:java.lang.Comparable
    • Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称 为类的自然排序。
    • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即 通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大 于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回 负整数,如果当前对象this等于形参对象obj,则返回零。
    • 实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有 序集合中的元素,无需指定比较器。
    • 对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。建议(虽然不是必需的)最好使自然排序与 equals 一致。
    • Comparable 的典型实现:(默认都是从小到大排列的)
      • String:按照字符串中字符的Unicode值进行比较
      • Character:按照字符的Unicode值来进行比较
      • 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
      • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
      • Date、Time等:后面的日期时间比前面的日期时间大
        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
        import java.util.Arrays;

        public class CompareTest {

        public static void main(String[] args) {

        Goods[] all = new Goods[4];
        all[0] = new Goods("《红楼梦》", 120);
        all[1] = new Goods("《水浒传》", 100);
        all[2] = new Goods("《西游记》", 150);
        all[3] = new Goods("《三国演义》", 80);

        Arrays.sort(all);

        System.out.println(Arrays.toString(all));

        }
        }

        class Goods implements Comparable {

        private String name;
        private double price;

        public Goods(String name, double price) {
        this.name = name;
        this.price = price;
        }

        @Override
        public String toString() {
        return "Goods{" +
        "name='" + name + '\'' +
        ", price=" + price +
        '}'+"\n";
        }

        @Override
        public int compareTo(Object o) {
        if (o instanceof Goods) {
        Goods other = (Goods) o;
        if (this.price > other.price) {
        return 1;
        } else if (this.price < other.price) {
        return -1;
        }
        return 0;
        }
        throw new RuntimeException("输入的数据类型不一致");
        }
        }

        点击查看运行结果
        1
        2
        3
        4
        5
        6
        7
        8
        D:\PATH-EN\java-path\bin\java.exe ...
        [Goods{name='《三国演义》', price=80.0}
        , Goods{name='《水浒传》', price=100.0}
        , Goods{name='《红楼梦》', price=120.0}
        , Goods{name='《西游记》', price=150.0}
        ]

        Process finished with exit code 0

  • 方式二:定制排序:java.util.Comparator
    • 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码, 或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序,强行对多个对象进行整体排 序的比较。
    • 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返 回正整数,则表示o1大于o2;如果返回0,表示等;返回负整数,表示 o1小于o2。
    • 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort), 从而允许在排序顺序上实现精确控制。
    • 还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。
      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
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      import java.util.Arrays;
      import java.util.Comparator;

      public class ComparatorTest {

      public static void main(String[] args) {
      Good[] all = new Good[4];

      all[0] = new Good("《红楼梦》", 120);
      all[1] = new Good("《水浒传》", 100);
      all[2] = new Good("《西游记》", 150);
      all[3] = new Good("《三国演义》", 80);

      Arrays.sort(all, new Comparator() {

      @Override
      public int compare(Object o1, Object o2) {
      Good g1 = (Good) o1;
      Good g2 = (Good) o2;

      return g1.getName().compareTo(g2.getName());
      }
      });

      System.out.println(Arrays.toString(all));

      }
      }

      class Good implements Comparable {

      private String name;
      private double price;

      public String getName() {
      return name;
      }

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

      public double getPrice() {
      return price;
      }

      public void setPrice(double price) {
      this.price = price;
      }

      public Good(String name, double price) {
      this.name = name;
      this.price = price;
      }

      @Override
      public String toString() {
      return "Goods{" +
      "name='" + name + '\'' +
      ", price=" + price +
      '}' + "\n";
      }

      @Override
      public int compareTo(Object o) {
      if (o instanceof Good) {
      Good other = (Good) o;
      if (this.price > other.price) {
      return 1;
      } else if (this.price < other.price) {
      return -1;
      }
      return 0;
      }
      throw new RuntimeException("输入的数据类型不一致");
      }
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      8
      D:\PATH-EN\java-path\bin\java.exe ...
      [Goods{name='《三国演义》', price=80.0}
      , Goods{name='《水浒传》', price=100.0}
      , Goods{name='《红楼梦》', price=120.0}
      , Goods{name='《西游记》', price=150.0}
      ]

      Process finished with exit code 0

System类

  • System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包。
  • 由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实 例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。
  • 成员变量

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

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

public static void main(String[] args) {
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);

}
}
点击查看运行结果
1
2
3
4
5
6
7
8
9
10
D:\PATH-EN\java-path\bin\java.exe ...
java的version:1.8.0_321
java的home:D:\PATH-EN\java-path\jre
os的name:Windows 11
os的version:10.0
user的name:Administrator
user的home:C:\Users\Administrator
user的dir:D:\学习\Study\java\JavaSenior

Process finished with exit code 0

Math类

  • 求最大值、最小值和绝对值

  • 求整运算

  • 三角函数运算

  • 指数运算

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
import java.util.Scanner;

public class MathTest {

public static void main(String[] args) {

System.out.println("10 和 20 的较大值:" + Math.max(10, 20));
System.out.println("15.6 和 15 的较小值:" + Math.min(15.6, 15));
System.out.println("-12 的绝对值:" + Math.abs(-12));

Scanner input = new Scanner(System.in);
System.out.println("请输入一个数字:");
double num = input.nextDouble();
System.out.println("大于或等于 " + num + " 的最小整数:" + Math.ceil(num));
System.out.println("小于或等于 " + num + " 的最大整数:" + Math.floor(num));
System.out.println("将 " + num + " 加上 0.5 之后最接近的整数:" + Math.round(num));
System.out.println("最接近 " + num + " 的整数:" + Math.rint(num));

System.out.println("90 度的正弦值:" + Math.sin(Math.PI / 2));
System.out.println("0 度的余弦值:" + Math.cos(0));
System.out.println("1 的反正切值:" + Math.atan(1));
System.out.println("120 度的弧度值:" + Math.toRadians(120.0));

System.out.println("4 的立方值:" + Math.pow(4, 3));
System.out.println("16 的平方根:" + Math.sqrt(16));
System.out.println("10 为底 2 的对数:" + Math.log10(2));
}
}
点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
D:\PATH-EN\java-path\bin\java.exe ...
1020 的较大值:20
15.615 的较小值:15.0
-12 的绝对值:12
请输入一个数字:
10
大于或等于 10.0 的最小整数:10.0
小于或等于 10.0 的最大整数:10.0
10.0 加上 0.5 之后最接近的整数:10
最接近 10.0 的整数:10.0
90 度的正弦值:1.0
0 度的余弦值:1.0
1 的反正切值:0.7853981633974483
120 度的弧度值:2.0943951023931953
4 的立方值:64.0
16 的平方根:4.0
10 为底 2 的对数:0.3010299956639812

Process finished with exit code 0

BigInteger与BigDecimal

  • BigInteger类

    • Integer类作为int的包装类,能存储的最大整型值为231-1,Long类也是有限的, 最大为263-1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类 都无能为力,更不用说进行运算了。
    • java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。 另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、 位操作以及一些其他操作。
    • 构造器
      • BigInteger(String val):根据字符串构建BigInteger对象
  • BigDecimal类

    • 一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中, 要求数字精度比较高,故用到java.math.BigDecimal类。
    • BigDecimal类支持不可变的、任意精度的有符号十进制定点数。

    • 构造器

      • public BigDecimal(double val)
      • public BigDecimal(String val)
    • 常用方法
      • public BigDecimal add(BigDecimal augend)
      • public BigDecimal subtract(BigDecimal subtrahend)
      • public BigDecimal multiply(BigDecimal multiplicand)
      • public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

第三章 枚举类与注解

枚举类的使用

  • 枚举类的实现
    • JDK1.5之前需要自定义枚举类
    • JDK 1.5 新增的 enum 关键字用于定义枚举类
  • 若枚举只有一个对象, 则可以作为一种单例模式的实现方式。
  • 枚举类的属性

    • 枚举类对象的属性不应允许被改动, 所以应该使用 private final 修饰
    • 枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
    • 若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数
  • 自定义枚举类定义步骤:

  1. 声明对象的属性:private final修饰
  2. 私有化类的构造器,并给对象属性赋值
  3. 提供当前枚举类的多个对象:public static final的
  4. 其他诉求1:获取枚举类对象的属性
  5. 其他诉求2:提供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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    public class EnumTest {

    public static void main(String[] args) {

    Season spring = Season.SPRING;
    Season summer = Season.SUMMER;
    Season autumn = Season.AUTUMN;
    Season winter = Season.WINTER;

    System.out.println(spring);
    System.out.println(summer);
    System.out.println(autumn);
    System.out.println(winter);

    }
    }

    class Season {

    //1.声明Season对象的属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;

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

    //3.提供当前枚举类的多个对象:public static final的
    public static final Season SPRING = new Season("春天", "春暖花开");
    public static final Season SUMMER = new Season("夏天", "夏日炎炎");
    public static final Season AUTUMN = new Season("秋天", "秋高气爽");
    public static final Season WINTER = new Season("冬天", "冰天雪地");

    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
    return seasonName;
    }

    public String getSeasonDesc() {
    return seasonDesc;
    }

    //4.其他诉求2:提供toString()
    @Override
    public String toString() {
    return "Season{" +
    "seasonName='" + seasonName + '\'' +
    ", seasonDesc='" + seasonDesc + '\'' +
    '}';
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    D:\PATH-EN\java-path\bin\java.exe ...
    Season{seasonName='春天', seasonDesc='春暖花开'}
    Season{seasonName='夏天', seasonDesc='夏日炎炎'}
    Season{seasonName='秋天', seasonDesc='秋高气爽'}
    Season{seasonName='冬天', seasonDesc='冰天雪地'}

    Process finished with exit code 0
  • 使用enum定义枚举类
  • 使用说明

    • 使用 enum 定义的枚举类默认继承了java.lang.Enum类,因此不能再继承其他类
    • 枚举类的构造器只能使用private权限修饰符
    • 枚举类的所有实例必须在枚举类中显式列出 ((, 分隔 ; 结尾 。列出的实例系统会自动添加 public static final 修饰
    • 必须在枚举类的第一行声明枚举类对象
  • JDK 1.5 中可以在 switch 表达式中使用 Enum 定义的枚举类的对象作为表达式 , case 子句可以直接使用枚举值的名字 , 无需添加枚举类作为限定。

  • 使用enum定义枚举类定义步骤:
  1. 提供当前枚举类的对象,多个对象之间用”,”隔开,末尾对象”;”结束
  2. 声明对象的属性:private final修饰
  3. 私有化类的构造器,并给对象属性赋值
  4. 其他诉求1:获取枚举类对象的属性
  5. 其他诉求2.如果需要的重写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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    public class EnumTest {

    public static void main(String[] args) {

    Season spring = Season.SPRING;
    Season summer = Season.SUMMER;
    Season autumn = Season.AUTUMN;
    Season winter = Season.WINTER;

    System.out.println(spring);
    System.out.println(summer);
    System.out.println(autumn);
    System.out.println(winter);
    }
    }

    enum Season {

    // 1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
    SPRING("春天", "春暖花开"),
    SUMMER("夏天", "夏日炎炎"),
    AUTUMN("秋天", "秋高气爽"),
    WINTER("冬天", "冰天雪地");

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

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

    // 4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
    return seasonName;
    }

    public String getSeasonDesc() {
    return seasonDesc;
    }

    // 5.其他诉求2.如果需要的重写toString()方法,默认返回常量的名

    @Override
    public String toString() {
    return getSeasonName();
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    D:\PATH-EN\java-path\bin\java.exe ...
    春天
    夏天
    秋天
    冬天

    Process finished with exit code 0
  • Enum类的主要方法
    1name返回此枚举常量的名称,在其枚举声明中对其进行声明。 与此方法相比,大多数程序员应该优先考虑使用 toString() 方法,因为 toString 方法返回更加用户友好的名称。该方法主要设计用于特殊情形,其正确性取决于获取正确的名称,其名称不会随版本的改变而改变。
    2ordinal返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)。 大多数程序员不会使用此方法。它被设计用于复杂的基于枚举的数据结构,比如 EnumSet 和 EnumMap。
    3toString返回枚举常量的名称,它包含在声明中。可以重写此方法,虽然一般来说没有必要。当存在更加“程序员友好的”字符串形式时,应该使用枚举类型重写此方法。
    4equals当指定对象等于此枚举常量时,返回 true。
    5hashCode返回枚举常量的哈希码。
    6clone抛出 CloneNotSupportedException。这可保证永远不会复制枚举,这对于保留其“单元素”状态是必需的。
    7compareTo比较此枚举与指定对象的顺序。在该对象小于、等于或大于指定对象时,分别返回负整数、零或正整数。 枚举常量只能与相同枚举类型的其他枚举常量进行比较。该方法实现的自然顺序就是声明常量的顺序。
    8getDeclaringClass返回与此枚举常量的枚举类型相对应的 Class 对象。当且仅当 e1.getDeclaringClass() == e2.getDeclaringClass() 时,两个枚举常量 e1 和 e2 的枚举类型才相同。(由该方法返回的值不同于由 Object.getClass() 方法返回的值,Object.getClass() 方法用于带有特定常量的类主体的枚举常量。)
    9valueOf返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。(不允许使用额外的空白字符。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EnumMethodTest {

public static void main(String[] args) {

// values(),返回枚举对象构成的数组
Thread.State[] values = Thread.State.values();

// toString(),调用枚举类的方法,可以再类中重写
for (Thread.State state : values) {
System.out.print(state.toString() + ",");
}

// 查看是否有相应对象名,有则返回,无则抛异常
Thread.State runnable = Thread.State.valueOf("RUNNABLE");
System.out.println("\n" + runnable);
}
}
点击查看运行结果
1
2
3
4
5
D:\PATH-EN\java-path\bin\java.exe ...
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED,
RUNNABLE

Process finished with exit code 0

  • 实现接口的枚举类
    • 和普通 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
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      public class EnumFlagTest {

      public static void main(String[] args) {

      Flag aTrue = Flag.TRUE;
      Flag aFalse = Flag.FALSE;

      System.out.println(aTrue);
      System.out.println(aFalse);

      Flag[] values = Flag.values();
      for (Flag flag:values) {
      flag.show();
      }
      }
      }

      enum Flag implements Info{

      TRUE("TRUE", "is true"){

      @Override
      public void show() {
      System.out.println("That is true!");
      }
      },
      FALSE("FALSE", "is false") {

      @Override
      public void show() {
      System.out.println("That is false!");
      }
      };

      private final String flagName;
      private final String flagDesc;

      Flag(String flagName, String flagDesc) {
      this.flagName = flagName;
      this.flagDesc = flagDesc;
      }

      public String getFlagName() {
      return flagName;
      }

      public String getFlagDesc() {
      return flagDesc;
      }

      @Override
      public String toString() {
      return flagName;
      }
      }

      interface Info {
      void show();
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      D:\PATH-EN\java-path\bin\java.exe ...
      TRUE
      FALSE
      That is true!
      That is false!

      Process finished with exit code 0

注解

  • 注解 (Annotation) 概述

    • 从 JDK 5.0 开始, Java 增加了对元数据(MetaData) 的支持, 也就是Annotation(注解)
    • Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加 载, 运行时被读取, 并执行相应的处理。通过使用 Annotation, 程序员 可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代 码分析工具、开发工具和部署工具可以通过这些补充信息进行验证 或者进行部署。
    • Annotation 可以像修饰符一样被使用, 可用于修饰包,类, 构造器, 方 法, 成员变量, 参数, 局部变量的声明, 这些信息被保存在 Annotation 的 “name=value” 对中。
    • 在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色,例如 用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗 代码和XML配置等。
    • 未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以 上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的 Struts2有一部分也是基于注解的了,注解是一种趋势,一定程度上 可以说:框架 = 注解 + 反射 + 设计模式。
    • 使用 Annotation 时要在其前面增加 @ 符号, 并把该Annotation 当成一个修饰符使用。用于修饰它支持的程序元素
  • 自定义Annotation

    • 定义新的Annotation 类型使用 @interface 关键字
    • 自定义注解自动继承java.lang.annotation.Annotation接口
    • Annotation 的成员变量在 Annotation 定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能 是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、 以上所有类型的数组。
    • 可以在定义 Annotation 的成员变量时为其指定初始值, 指定成员变量的初始值可使用 default 关键字
    • 如果只有一个参数成员,建议使用参数名为value
    • 如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认 值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”
    • 没有成员定义的 Annotation 称为标记; 包含成员变量的 Annotation 称为元数据Annotation
      注意:自定义注解必须配上注解的信息处理流程才有意义。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import java.lang.annotation.*;

      @MyAnnotation(value = "Jermyn")
      public class AnnotationTest {
      public static void main(String[] args) {
      Class clazz = AnnotationTest.class;
      Annotation a = clazz.getAnnotation(MyAnnotation.class);
      MyAnnotation m = (MyAnnotation) a;
      String info = m.value();
      System.out.println(info);
      }
      }

      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      // 1.注解声明为:@interface
      @interface MyAnnotation {

      // 2.内部定义成员,通常使用value表示
      // 3.可以指定成员的默认值,使用default定义
      String value() default "JERMYN";
      }

      点击查看运行结果
      1
      2
      3
      4
      D:\PATH-EN\java-path\bin\java.exe ...
      Jermyn

      Process finished with exit code 0

  • JDK 中的元注解
  • JDK 的元Annotation 用于修饰其他Annotation 定义
  • JDK5.0提供了4个标准的meta-annotation类型,分别是:

    • Retention
    • Target
    • Documented
    • Inherited

      个人理解:元注解就是对现有注解进行解释说明的注解,类比 String name = “Jermyn”;String 是数据类型,name是数据的名称,”Jermyn”就是数据,String和name就是元数据,”Jermyn”就是数据。

  • @Retention

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

}

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2 {

}

个人理解:RetentionPolicy修饰注解的生命周期就是什么时候”死”,RetentionPolicy的三种状态,SOURCE,ClASS,RUNTIME,简单理解为以此来修饰的注解的生命周期,在某种状态时可以被使用,即再某种状态后会被抛弃,还可以理解为,如果修饰的注解在运行时还会起作用就不能用前两种修饰。如果在编译的时候起作用,运行时无关紧要就可以用CLASS修饰,所以如果想要反射时可以检测到,就用RUNTIME。


  • @Target:
    @Target: 用于修饰 Annotation 定义, 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。 @Target 也包含一个名为 value 的成员变量。
    点击图片可以跳转源码

    • value的取值
      点击图片可以跳转源码
1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE,TYPE_PARAMETER,TYPE_USE})
public @interface MyAnnotation {

String value() default "Jermyn";
}

个人理解:元注解Target就是解释注解可以修饰那些元素,即写在那些元素的上面


  • 其余两个元注解用较少

    • @Documented: 用于指定被该元 Annotation 修饰的 Annotation 类将被javadoc 工具提取成文档。默认情况下,javadoc是不包括注解的。定义为Documented的注解必须设置Retention值为RUNTIME。

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

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

第四章 Java集合

Java集合框架概述

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

    • 数组在内存存储方面的特点:

      • 数组初始化以后,长度就确定了。
      • 数组声明的类型,就决定了进行元素初始化时的类型
    • 数组在存储数据方面的弊端:

      • 数组初始化以后,长度就不可变了,不便于扩展
      • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数
      • 数组存储的数据是有序的、可以重复的。——>存储数据的特点单一
  • Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的 关联数组。

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

    • Collection接口:单列数据,定义了存取一组对象的方法的集合

      • List:元素有序、可重复的集合
      • Set:元素无序、不可重复的集合
    • Map接口:双列数据,保存具有映射关系“key-value对”的集合

Collection接口方法

  • Collection 接口
    • Collection接口是List、Set和Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。
    • JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
    • 在Java5之前,Java集合会丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理从JDK5.0增加了泛型以后,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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    import org.junit.Test;

    import java.util.*;

    public class CollectionTest {

    Collection collection = new ArrayList();
    Collection collection01 = new ArrayList();

    @Test
    public void addTest() {

    // add(Object e),添加的数据可以时任何类型
    collection.add("Jermyn");
    collection.add('c');
    collection.add(123);
    collection.add(new Object());

    System.out.println(collection.size());

    // addAll()
    collection01.add(new Date());
    collection01.addAll(collection);

    System.out.println(collection.size());
    System.out.println(collection01.size());

    System.out.println(collection01.toString());
    }

    @Test
    public void isEmptyTest() {

    // 判断集合是否为空
    System.out.println(collection.isEmpty());

    collection.add("jermyn");

    System.out.println(collection.isEmpty());
    }

    @Test
    public void clearTest() {
    collection.add("jermyn");
    collection.add("https://jermyn.cn/");
    System.out.println(collection.isEmpty());

    // 清空集合
    collection.clear();
    System.out.println(collection.isEmpty());
    }

    @Test
    public void containsTest() {
    collection.add("jermyn");
    collection.add("https://jermyn.cn/");
    collection.add(new String("JERMYN"));

    // 是否包含obj
    System.out.println(collection.contains("Jermyn"));
    System.out.println(collection.contains("jermyn"));

    // 结果为true,则比较的是内容,显然地址值是不同,实际是上是调用 equals 方法
    // 如果自己定义的类,然后比较的实例是否包含,类比String可知是调用 equals 方法,
    // 但是如果自己定义的类没有重写equals方法,就是调用Object的equals方法,而Object的equals
    // 方法就是"=="比较的是地址值,所以是false,如果重写equals则是true
    System.out.println(collection.contains(new String("JERMYN")));


    Collection collection01 = Arrays.asList("jermyn",
    "https://jermyn.cn/",
    new String("JERMYN"),
    25);

    // containsAll:判断collection中的数据是否都在collection01 中
    System.out.println(collection01.containsAll(collection));

    collection.add("jer");
    System.out.println(collection01.containsAll(collection));

    }

    @Test
    public void removeTest() {

    collection.add("Jermyn");
    collection.add('c');
    collection.add(123);
    collection.add(new Object());

    // remove(Object obj): 找到需要移除的数据返回true,反之返回false
    System.out.println(collection.remove("jer"));
    System.out.println(collection);

    System.out.println(collection.remove("Jermyn"));
    System.out.println(collection);

    // removeAll(Collection c):从当前集合中移除 c 中所有的元素
    // 类比 containsAll,不演示
    }

    @Test
    public void retainAll() {

    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(789);
    coll.add(false);

    //5.retainAll(Collection coll1):交集:获取当前集合和coll1集合的交集,并返回给当前集合
    Collection coll1 = Arrays.asList(123, 456, 789);
    coll.retainAll(coll1);
    System.out.println(coll);
    }

    @Test
    public void equalsTest() {

    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);

    //equals(Object obj):要想返回true,需要当前集合和形参集合的元素都相同。
    Collection coll1 = new ArrayList();
    coll1.add(456);
    coll1.add(123);
    coll1.add(new String("Tom"));
    coll1.add(false);

    System.out.println(coll.equals(coll1));
    }

    @Test
    public void hashCodeTest() {
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new String("Tom"));
    coll.add(false);

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

    @Test
    public void toArrayTest() {

    collection.add(123);
    collection.add("Jermyn");
    collection.add('j');
    collection.add(new String("JERMYN"));
    collection.add(new Date());

    // toArray():将集合转换为数组,因为集合都是Object 的实例,所以集合就是Object[]
    // 集合-->数组
    Object[] objects = collection.toArray();
    for (Object obj : objects) {
    System.out.print(obj + ",");
    }

    // 数组-->集合
    // 调用Arrays类的静态方法asList()
    List list = Arrays.asList("Jermyn", 'j', new Date(), new String("JERMYN"));
    System.out.println();
    System.out.println(list);
    }

    @Test
    public void iteratorTest() {
    // iterator():返回Iterator接口的实例,用于遍历集合元素。放在IteratorTest.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
    D:\PATH-EN\java-path\bin\java.exe ...
    false
    true
    true
    true
    false
    123,Jermyn,j,JERMYN,Fri Oct 14 19:42:14 CST 2022,
    [Jermyn, j, Fri Oct 14 19:42:14 CST 2022, JERMYN]
    [123, 456, 789]
    false
    true
    4
    4
    5
    [Fri Oct 14 19:42:14 CST 2022, Jermyn, c, 123, java.lang.Object@61e717c2]
    true
    false
    false
    false
    [Jermyn, c, 123, java.lang.Object@1b9e1916]
    true
    [c, 123, java.lang.Object@1b9e1916]
    7639761

    Process finished with exit code 0

Iterator迭代器接口

  • 使用 Iterator 接口遍历集合元素

    • Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元 素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公 交车上的售票员”、“火车上的乘务员”、“空姐”。
    • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所 有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了 Iterator接口的对象。
    • 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
    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    import org.junit.Test;

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Iterator;

    public class IteratorTest {
    // iterator():返回Iterator接口的实例,用于遍历集合元素。

    @Test
    public void hasNextTest() {

    Collection collection = new ArrayList();

    collection.add(123);
    collection.add(456);
    collection.add(new String("Tom"));
    collection.add(false);

    // 方式一:
    System.out.println("*******************方式一*******************");
    Iterator iterator = collection.iterator();
    System.out.println(iterator.next());
    System.out.println(iterator.next());
    System.out.println(iterator.next());
    System.out.println(iterator.next());
    // 报错
    // System.out.println(iterator.next());

    // 方式二:
    System.out.println("*******************方式二*******************");
    Iterator iterator02 = collection.iterator();
    for (int i = 0; i < collection.size(); i++) {
    System.out.println(iterator02.next());
    }

    //方式三:
    System.out.println("*******************方式三*******************");
    Iterator iterator03 = collection.iterator();
    while (iterator03.hasNext()) {

    //next():①指针下移 ②将下移以后集合位置上的元素返回
    System.out.println(iterator03.next());
    }
    }

    @Test
    public void removeTest() {
    Collection collection = new ArrayList();

    collection.add(123);
    collection.add(456);
    collection.add(new String("Tom"));
    collection.add(false);

    Iterator iterator = collection.iterator();

    // 删除集合中的"Tom"
    while (iterator.hasNext()) {
    Object obj = iterator.next();

    if ("Tom".equals(obj)) {
    iterator.remove();
    }
    }

    // 遍历集合
    Iterator iterator01 = collection.iterator();
    while (iterator01.hasNext()) {
    System.out.println(iterator01.next());
    }
    }
    }

    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    D:\PATH-EN\java-path\bin\java.exe ...
    *******************方式一*******************
    123
    456
    Tom
    false
    *******************方式二*******************
    123
    456
    Tom
    false
    *******************方式三*******************
    123
    456
    Tom
    false
    *******************removeTest*******************
    123
    456
    false

    Process finished with exit code 0
  • 使用 foreach 循环遍历集合元素

    • Java 5.0 提供了 foreach 循环迭代访问 Collection和数组。
    • 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
    • 遍历集合的底层调用Iterator完成操作。
    • foreach还可以用来遍历数组。
      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
      import java.util.ArrayList;
      import java.util.Collection;

      public class ForEachTest {

      public static void main(String[] args) {

      // 示例一
      int[] arr = new int[]{1, 2, 3, 4, 5, 6};
      //for(数组元素的类型 局部变量 : 数组对象)
      for (int i : arr) {
      System.out.println(i);
      }

      // 示例二
      Collection coll = new ArrayList();
      coll.add(123);
      coll.add(456);
      coll.add(new String("Tom"));
      coll.add(false);

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

      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      D:\PATH-EN\java-path\bin\java.exe ...
      1
      2
      3
      4
      5
      6
      123
      456
      Tom
      false

      Process finished with exit code 0

Collection子接口之一:List接口

  • List接口概述

    • 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
    • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
    • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据 序号存取容器中的元素。
    • JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。
  • List接口方法

    • List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
      • void add(int index, Object ele):在index位置插入ele元素
      • boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
      • Object get(int index):获取指定index位置的元素
      • int indexOf(Object obj):返回obj在集合中首次出现的位置
      • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
      • Object remove(int index):移除指定index位置的元素,并返回此元素
      • Object set(int index, Object ele):设置指定index位置的元素为ele
      • List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
        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
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        import org.junit.Test;

        import java.util.*;

        public class ListTest {

        @Test
        public void test01() {

        ArrayList arrayList = new ArrayList();
        arrayList.add(123);
        arrayList.add("");
        arrayList.add("Jermyn");
        arrayList.add(new Date());
        System.out.println(arrayList);

        System.out.println("***************************************");

        // add(index,element):在index之后插入element
        arrayList.add(1, "JERMYN");
        System.out.println(arrayList);

        List integers = Arrays.asList(1, 2, 3);

        // addAll(int index,Collection c):从index位置开始将c中的所有元素添加进来
        boolean b = arrayList.addAll(1, integers);

        System.out.println(arrayList.size());

        // get(int index):获取指定index位置的元素
        Object o = arrayList.get(6);
        System.out.println(o);
        }

        @Test
        public void test02() {

        ArrayList arrayList = new ArrayList();
        arrayList.add(123);
        arrayList.add("");
        arrayList.add("Jermyn");
        arrayList.add(123);
        arrayList.add(new Date());
        System.out.println(arrayList);

        // int indexOf(Object obj):返回obj在集合中首次出现的位置,有返回index
        int i = arrayList.indexOf("");
        System.out.println(i);

        // 没有返回-1
        int index = arrayList.indexOf("JERMYN");
        System.out.println(index);

        // int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置,没有返回-1
        int index1 = arrayList.lastIndexOf(123);
        System.out.println(index1);

        // Object remove(int index):移除指定index位置的元素,并返回此元素(重载了Collection的remove方法)
        Object remove = arrayList.remove(4);
        System.out.println(arrayList);

        // remove(Object o):Collection的remove删除指定元素
        boolean remove1 = arrayList.remove("Jermyn");
        System.out.println(arrayList);

        // Object set(int index, Object ele):设置指定index位置的元素为ele
        Object set = arrayList.set(1, "https://jermyn.cn");
        System.out.println(arrayList);

        arrayList.add(1);
        arrayList.add(13);
        arrayList.add(12);

        // List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
        List list = arrayList.subList(2, 5);
        System.out.println(arrayList);
        System.out.println(list);
        }

        @Test
        public void test03() {

        ArrayList arrayList = new ArrayList();
        arrayList.add(123);
        arrayList.add("");
        arrayList.add("Jermyn");
        arrayList.add(123);
        arrayList.add(new Date());
        arrayList.add(1);
        arrayList.add(13);
        arrayList.add(12);

        // 遍历方式一:Iterator迭代器方式
        Iterator iterator = arrayList.iterator();

        System.out.print("方式一:");
        while (iterator.hasNext()) {
        System.out.print(iterator.next() + ",");
        }

        // 遍历方式二:增强for循环
        System.out.println();
        System.out.print("方式二:");
        for (Object o : arrayList) {
        System.out.print(o + ",");
        }

        // 遍历方式三:普通for循环
        System.out.println();
        System.out.print("方式三:");
        for (int i = 0; i < arrayList.size(); i++) {
        System.out.print(arrayList.get(i) + ",");
        }
        }
        }

        点击查看运行结果
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        D:\PATH-EN\java-path\bin\java.exe ...
        [123, , Jermyn, Sat Oct 15 18:13:24 CST 2022]
        ***************************************
        [123, JERMYN, , Jermyn, Sat Oct 15 18:13:24 CST 2022]
        8
        Jermyn
        [123, , Jermyn, 123, Sat Oct 15 18:13:24 CST 2022]
        1
        -1
        3
        [123, , Jermyn, 123]
        [123, , 123]
        [123, https://jermyn.cn, 123]
        [123, https://jermyn.cn, 123, 1, 13, 12]
        [123, 1, 13]
        方式一:123,,Jermyn,123,Sat Oct 15 18:13:24 CST 2022,1,13,12,
        方式二:123,,Jermyn,123,Sat Oct 15 18:13:24 CST 2022,1,13,12,
        方式三:123,,Jermyn,123,Sat Oct 15 18:13:24 CST 2022,1,13,12,
        Process finished with exit code 0

        小练习

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        import org.junit.Test;

        import java.util.ArrayList;
        import java.util.List;

        public class ListExer {

        @Test
        public void testListRemove() {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updateList(list);
        System.out.println(list);//
        }

        private static void updateList(List list) {
        list.remove(2);
        list.remove(new Integer(1));
        }
        }
        点击查看运行结果
        1
        2
        3
        4
        D:\PATH-EN\java-path\bin\java.exe ...
        [2]

        Process finished with exit code 0
  • List实现类之一:ArrayList

  • ArrayList 是 List 接口的典型实现类、主要实现类本质上,ArrayList是对象引用的一个”变长”数组
  • ArrayList的JDK1.8之前与之后的实现区别?

    • JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
    • JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元 素时再创建一个始容量为10的数组
  • Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合


个人理解,有错误的请指教

JDK1.7的ArrayList的扩容:底层是用数组存储Object类型的数组,声明为Object[] elementDate,创建当前
ArrayList list = new ArrayList 对象,使用new + 类的构造器创建。初始化当前的底层的数组, 数组的容量即initialCapacity=10,然后进行添加操作调用 add() 方法,添加的内容就是Object类型的数据,添加之前先确定数组的容量是否够,即是否能加进去,源码显示ensureCapacityInternal(size + 1),第一次执行add的话size就是 0。size的取值就是调用size(),这个方法返回就是add几个。前面的确认容量机制会判断add的是否大于底层初始的容量就是默认的10。如果底层数组够的话就不用扩容。一直就可以add,如果总长度大于10的话,就进行扩容,源码显示:grew(minCapacity)。此时的minCapacity就是底层数组长度+add的数组长度。扩容的操作是:先把minCapacity.length赋值给oldCapacity。然后创建新的长度(newCapacity)。这个新的长度为原来的1.5倍,源码显示为:oldCapacity+(oldCapacity >> 1),此时会执行几个判断①新扩容的是否小于刚拿来的minCapacity长度,小的话就直接把minCapacity的长度给newCapacity。②如果发现扩容1.5背还不能存储add的下一个数组,数组的长度都快到数组能够存储的最大长度(MAX_ARRAY_SIZE)了,这是就new一个数组新的数组长度,这个数组的长度使用hugeCapacity(minCapacity)来赋值,在这个方法中会判断是否是整形的能够声明的最大值。如果还不够用,就报错,这个Error叫做OutOfMemoryError。总的来说①②这两个情况很少出现,可以说几乎不会出现。仔细想就是一般不会有比整形最大值还大的数组长度了。接前面,扩充完之后就是需要将原来数组的内容copy给扩充1.5倍后的数组了。源码显示就是elementDate = Arrays.copyOf(elementDate newCapacity),至此数组的扩容就完成了。


提示:在开发中,大概可以确定数组存储多少个元素。或者可以基本确定会add多少次到达多少元素的话,我们定义的时候就不要使用空参的构造器了(空参构造器默认数组长度为10),我们使用带参的构造器,例如我预估数组长度或达到100的话就new ArrayList(100),这个100就是initialCapacity。目的是频繁的扩容会频繁的复制数组造成效率低。


JDK1.8的ArrayList的扩容:开始时我们通过空参的构造器创建对象,1.8中并没有像1.7中直接new一个长度为10的数组,而是elementDate指向一个常量即:DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这个常量的特点就是它等于{},即为空,也就是,new的时候没有直接生成数组,而是指向{}。这种方式显然比1.7的好,不用直接占用内存创建数组。那么在什么时候创建呢,就是在第一次执行add操作的时候。执行add操作的时候首先也是ensureCapacityInternal(size + 1),显然时不够,因为没有容量。进入这个方法后,首先的操作就是判断elementDate是否等于{}即DEFAULTCAPACITY_EMPTY_ELEMENTDATA,显然第一次的时候是等于的。这时会给minCapacity复制源码显示:minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity),这个DEFAULT_CAPACITY就是10。minCapacity就是0,这都是对于第一次来说。此时的minCapacity取最大的就是DEFAULT_CAPACITY即为10。下一步执行ensureExplicitCacity()方法将minCapacity传入。进入这个方法后会判断minCapacity的大小,如果比elementDate.length大,就执行grew(minCapacity),显然是比elementDate.length大的,因为第一add的时候elementDate.length为0。然后就进行扩容操作,进入grew()后,首先将oldCapacity赋值为elementDate.length即为0。newCapacity为oldCapacity+(oldCapacity >> 1),所以newCapacity也是0(0扩大1.5倍还是0)。然后执行判断,newCapacity和minCapacity的大小,newCapacity显然小于minCapacity,因为前者为0,后者为10。此时把minCapacity赋值给newCapacity,则newCapacity为10。然后就给elementDate进行copy的操作,elementDate就从原来的{}变为Arrays.copyOf(elementDate newCapacity)。成为一个数组长度为10的数组。此时数组创建完成。如果扩容的话就从前面的步骤在执行一遍,有相应的判断来跳转。


JDK1.7和JDK1.8的最大区别就就是:前者的第一次扩容是在数组长度大于10的时候,后者是在第一次add操作时。前者的第一次扩容是10到15,后者是0到10。


JDK1.8的ArrayList的注释没有修改,沿用JDK1.7的,所以呢,你提交了,你就是JDK的源码贡献者!!

点击图片阅读ArrayList.java源码


  • List实现类之二:LinkedList

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

      • void addFirst(Object obj)
      • void addLast(Object obj)
      • Object getFirst()
      • Object getLast()
      • Object removeFirst()
      • Object removeLast()
    • LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基 本结构。Node除了保存数据,还定义了两个变量:

      • prev变量记录前一个元素的位置
      • next变量记录下一个元素的位置

个人理解,有错误的请指教

个人理解:JDK1.7和JDK1.8LinkedList源码一样,也不涉及到扩容,在次只做底层的源码分析。LinkedList为双向链表,顾名思义就是两个方向,这个方向分别指向前一个后一个元素的地址值。LinkedList不像ArrayList是Object[] 存储,而是以一种Node存储。Node在源码中封装为内部类,只供在内部使用。Node的核心结构分为三部分。Node(Nodeprev, E element, Nodenext),我们添加的数据就是E element。当我们new LinkedList后。会有一个变量叫first它指向我们创建链条的头一个即第一个元素。另外一个变量叫last它指向我们创建链条的最后元素。正常的添加是在last后添加,查找的话就是从first往后查找。当我们添加元素时(即执行add操作)。调用add里面的linkLast方法并把需要添加的元素e作为参数放进去。linkLast方法中。把当前链表的最后一个元素即last给Node 变量名为l。如果第一次的话last就是null。下一步新建一个Node,新建的Node的prev指向last,element就是添加的元素值。next指向null。这时我们刚new的Node就成了整个链表的最后一个元素。接下来做一个判断。①判断l是否时null。如果为null则说明之前时没有添加过元素的。②否则也是就之前添加过元素。就把刚才新建的Node指向l.next。这个l是刚才没有添加元素前原链表的next属性。对于原链表的last元素的next属性原来是null的。现在new完新Node,新的Node的next就成了null。至此LinkedList的添加操作就完成了。对于插入和删除方式和添加操作类似。


小结:理解了LinkedList源码后,就很容易推出LinkedList的优点,就是插入和删除会更方便。插入的话只需要知道插入位置和插入位置下一个元素就可以,对于之前和之后的链表都不影响。不用像ArrayList每次插入删除频繁的前移或者后移。


点击图片可以跳转源码


  • List 实现类之三:Vector
    • Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
    • 在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
    • 新增方法:
      • void addElement(Object obj)
      • void insertElementAt(Object obj,int index)
      • void setElementAt(Object obj,int index)
      • void removeElement(Object obj)
      • void removeAllElements()
个人理解,有错误的请指教

Vector特点:作为List接口的古老实现类;线程安全的,效率低。Vector和ArrayList源码相似,因为古老所以不建议使用了,一般都是用ArrayList,但是ArrayList线程不安,如果遇到线程安全的情况,就使用Collection class的synchronizedList返回一个线程安全的ArrayList;jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。不作具体的分析了。


Collection子接口之二:Set接口

  • Set 接口概述

    • Set接口是Collection的子接口,set接口没有提供额外的方法
    • Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
    • Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
  • Set实现类之一:HashSet

    • HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
    • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除
      性能。
    • HashSet 具有以下特点:
      • 不能保证元素的排列顺序
      • HashSet 不是线程安全的
    • 集合元素可以是 null

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

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

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

  • 重写 equals() 方法的基本原则
    以自定义的Customer类为例,何时需要重写equals()?

  • 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不 同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法, 它们仅仅是两个对象。
  • 因此,违反了“相等的对象必须具有相等的散列码”。
  • 结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

  • 问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

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

      个人理解:HashSet的无序性不等于随机性。HashSet开始时创建的底层数组长度为16源码:DEFAULT_INITIAL_CAPACITY = 1 << 4。添加的过程并非按照数组的索引值存入,而是依据数据的哈希来存储。Set存储数据讲究无序性,存储的方式就是,每个数据生成一个哈希值,这个哈希值就决定了这个数据在数组中的存放位置。具体的存放位置也是通过某种散列函数或者称为某种算法来决定具体位置的。再进行add操作的时候也是类似的方法,算出哈希值,通过某种算法算出具体位置。这个时候就会出现两个个问题就是,①两个数据的哈希值可能相同,但是通过算法处理后的具体位置可能时相同的。这个时候就会判断这个位置上是否有元素。没有的话就直接放在这个位置,有的话这是就引入链表的结构了。JDK1.7和JDK1.8的区别是不同的,对于JDK1.7来说就是把后者相同具体存放位置的元素(即新元素)放在前者相同具体存放位置的元素的位置(旧元素),然后让新元素指向旧元素,。JDK1.8就是拿前者相同具体存放位置的元素指向后者相同具体存放位置的元素。(简记为:“七上八下”七是新的元素在上边,旧的在下边,八是新的元素在下边,旧的元素在上边)。挺难描述的,但是不难理解。②两个数据可能相同,经过某种算法算出具体存放位置相同。这个情况下开始时就会调用equals方法判断数据是否相同。相同的话就添加失败,不同的话就是上面的“七上八下”。好处就在于,添加的过程为保证不可重复性,就不同所有都遍历判断是否相同,只判断对应位置的数据是否相同,同就false,不同就True,添加成功。

      个人理解可能还是有偏差的,可以查看尚硅谷宋红康老师的详细的讲解总结:

  • Set实现类之二:LinkedHashSet
    • LinkedHashSet 是 HashSet 的子类
    • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入 顺序保存的。
    • LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全 部元素时有很好的性能。
    • LinkedHashSet 不允许集合元素重复。

      个人理解:LinkedSet的有序是一种伪有序,存储的方式都是:数组+链表的结构。LinkedSet营造的有序简单理解就是固定的使双向链表的prev和next指向上一个add的数据和下一个add的数据。这就营造出了有序的状态


  • Set实现类之三:TreeSet

    • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
    • TreeSet底层使用红黑树结构存储数据
    • 新增的方法如下: (了解)
      • Comparator comparator()
      • Object first()
      • Object last()
      • Object lower(Object e)
      • Object higher(Object e)
      • SortedSet subSet(fromElement, toElement)
      • SortedSet headSet(toElement)
      • SortedSet tailSet(fromElement)
        TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
  • 排序—自然排序

  • 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元 素之间的大小关系,然后将集合元素按升序(默认情况)排列
  • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable接口。
    • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
  • Comparable 的典型实现:

    • BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小 进行比较
    • Character:按字符的 unicode值来进行比较
    • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
    • String:按字符串中字符的 unicode 值进行比较
    • Date、Time:后边的时间、日期比前面的时间、日期大
  • 向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添 加的所有元素都会调用compareTo()方法进行比较。

  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同 一个类的对象。
  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通 过 compareTo(Object obj) 方法比较返回值。
  • 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保 证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过 equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。 否则,让人难以理解。

  • 排序—定制排序

    • TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没 有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照 其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来 实现。需要重写compare(T o1,T o2)方法。
    • 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
    • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构 造器。
    • 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异 常。
    • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0

test01为自然排序,test02为定制排序

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import org.junit.Test;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Objects;
import java.util.TreeSet;

public class TreeSetTest {


@Test
public void test01() {
TreeSet set = new TreeSet();

// TreeSet 添加的数据都是同一类型
set.add(new User("JERMYN", 18));
set.add(new User("JERRY", 21));
set.add(new User("JACK", 14));
set.add(new User("JACK", 26));
set.add(new User("MARY", 32));
set.add(new User("TOM", 14));

Iterator iterator = set.iterator();
while (iterator.hasNext()) {
// 从小到大的顺序排序
System.out.println(iterator.next());
}
}

@Test
public void test02() {

// 定制排序
Comparator comparator = new Comparator() {
@Override
public int compare(Object o1, Object o2) {

if (o1 instanceof User && o2 instanceof User) {
User u1 = (User) o1;
User u2 = (User) o2;
return Integer.compare(u1.age, u2.age);
} else {
throw new RuntimeException("输入有误");
}
}
};


// 有参按参数重写 compare 来
TreeSet set = new TreeSet(comparator );

set.add(new User("JERMYN", 18));
set.add(new User("JERRY", 21));
set.add(new User("JACK", 14));
set.add(new User("JACK", 26));
set.add(new User("MARY", 32));
set.add(new User("TOM", 14));

Iterator iterator = set.iterator();

while (iterator.hasNext()) {
// 从小到大的顺序排序
System.out.println(iterator.next());
}
}
}

class User implements Comparable {
String name;
int age;

public User() {
}

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

@Override
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("输入的类型不匹配");
}
}

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

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

@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 Objects.equals(name, user.name);
}
}

点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
D:\PATH-EN\java-path\bin\java.exe ...
------------------test01--------------------
User{name='TOM', age=14}
User{name='MARY', age=32}
User{name='JERRY', age=21}
User{name='JERMYN', age=18}
User{name='JACK', age=14}
User{name='JACK', age=26}
------------------test02--------------------
User{name='JACK', age=14}
User{name='JERMYN', age=18}
User{name='JERRY', age=21}
User{name='JACK', age=26}
User{name='MARY', age=32}

Process finished with exit code 0

Map接口

  • Map接口概述

    • Map与Collection并列存在。用于保存具有映射关系的数据:key-value
    • Map 中的 key 和 value 都可以是任何引用类型的数据
    • Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
    • 常用String类作为Map的“键”
    • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
    • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和 Properties。其中,HashMap是 Map 接口使用频率最高的实现类
  • Map接口:常用方法

    • 添加、删除、修改操作:
      • Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
      • void putAll(Map m):将m中的所有key-value对存放到当前map中
      • Object remove(Object key):移除指定key的key-value对,并返回value
      • void clear():清空当前map中的所有数据
    • 元素查询的操作:
      • Object get(Object key):获取指定key对应的value
      • boolean containsKey(Object key):是否包含指定的key
      • boolean containsValue(Object value):是否包含指定的value
      • int size():返回map中key-value对的个数
      • boolean isEmpty():判断当前map是否为空
      • boolean equals(Object obj):判断当前map和参数对象obj是否相等
    • 元视图操作的方法:
      • Set keySet():返回所有key构成的Set集合
      • Collection values():返回所有value构成的Collection集合
      • Set entrySet():返回所有key-value对构成的Set集合
        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
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        import org.testng.annotations.Test;

        import java.util.*;

        public class MapTest {

        Map hashMap = new HashMap();
        Map map = new HashMap();

        @Test
        public void AddRemoveModifyTest() {

        // 添加
        hashMap.put("A", 18);
        hashMap.put("C", 15);
        hashMap.put("B", 13);
        // 修改
        hashMap.put("A", 20);

        System.out.println(hashMap);

        map.put("a", 26);
        map.put("e", 16);
        map.put("d", 6);

        hashMap.putAll(map);
        System.out.println(hashMap);

        // 移除
        Object a = hashMap.remove("A");
        System.out.println(a);
        System.out.println(hashMap);
        // 不存在返回null
        System.out.println(hashMap.remove("D"));

        // 清空
        hashMap.clear();
        System.out.println(hashMap);
        System.out.println(hashMap.size());
        }

        @Test
        public void InquireTest() {

        hashMap.put("A", 18);
        hashMap.put("C", 15);
        hashMap.put("B", 13);

        // 有则key.value
        System.out.println(hashMap.get("A"));

        // 没有则null
        System.out.println(hashMap.get("D"));

        // 是否有key,有则true,反之则false
        boolean isExistKey = hashMap.containsKey("D");
        System.out.println(isExistKey);

        // 是否有value,有则true,反之则false
        boolean isExistValue = hashMap.containsValue("13");
        System.out.println(isExistValue);

        // 是否是空
        hashMap.clear();
        System.out.println(hashMap.isEmpty());
        }

        @Test
        public void MetaViewTest() {

        hashMap.put("A", 18);
        hashMap.put("C", 15);
        hashMap.put("B", 13);

        // 遍历key
        Set set = hashMap.keySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
        System.out.println(iterator.next());
        }
        System.out.println();
        // 遍历value
        Collection values = hashMap.values();
        for (Object o : values) {
        System.out.println(o);
        }

        System.out.println();

        // 遍历所有的key-value
        Set entrySet = hashMap.entrySet();
        Iterator iterator1 = entrySet.iterator();
        for (int i = 0; i < entrySet.size(); i++) {
        System.out.println(iterator1.next());
        }
        }
        }

        点击查看运行结果
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        D:\PATH-EN\java-path\bin\java.exe ...
        A
        B
        C

        18
        13
        15

        A=18
        B=13
        C=15

        ===============================================
        Default Suite
        Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
        ===============================================


        Process finished with exit code 0

  • Map实现类之一:HashMap

    • HashMap是 Map 接口使用频率最高的实现类。
    • 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
    • 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
    • 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
    • 一个key-value构成一个entry
    • 所有的entry构成的集合是Set:无序的、不可重复的
    • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
    • HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
  • HashMap的存储结构
    JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
    JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。

  • HashMap源码中的重要常量

    HashMap源码解析

    • DEFAULT_INITIAL_CAPACITY : HashMap 的 默 认 容 量 ,16
    • MAXIMUM_CAPACITY : HashMap 的 最 大 支 持 容 量 ,2^30
    • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子
    • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树
    • UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
    • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行 resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 倍。)
    • table:存储元素的数组,总是2的n次幂
    • entrySet:存储具体元素的集
    • size:HashMap中存储的键值对的数量
    • modCount:HashMap扩容和结构改变的次数。
    • threshold:扩容的临界值,=容量*填充因子
    • loadFactor:填充因子

  • HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时, 系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量 (Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个 bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
  • 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引 用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 而且新添加的元素作为链表的head。
  • 添加元素的过程:
    向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据 key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数 组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上 已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次 比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果 hash值不同,继续比较二者是否equals。如果返回值为true,则使用entry1的value 去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都 为false,则entry1仍可添加成功。entry1指向原有的entry元素。

  • HashMap的扩容
    当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的 长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在 HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算 其在新数组中的位置,并放进去,这就是resize。

  • 那么HashMap什么时候进行扩容呢?
    当HashMap 中的元素个数超过数组大小( 数组总大小length, 不是数组中个数 size) loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况 下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数 超过16 0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把 数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。

  • HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个 HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系 时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表 中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查 找bucket中的元素。

  • 每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带 一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能 生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象 可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个 TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

  • 那么HashMap什么时候进行扩容和树形化呢?

    • 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数 size) loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认 情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中 元素个数超过16 0.75=12(这个值就是代码中的threshold值,也叫做临界值) 的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元 素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
    • 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有 达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成 树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后, 下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
  • 关于映射关系的key是否可以修改?answer:不要修改
    映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算 每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关 系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

  • 总结:JDK1.8相较于之前的变化:

  1. HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
  2. 当首次调用map.put()时,再创建长度为16的数组
  3. 数组为Node类型,在jdk7中称为Entry类型
  4. 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
  5. 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置 上的所有key-value对使用红黑树进行存储。
个人理解,有错误的请指教

HashMap实例化:new + 类的构造器。源码显示this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);这两个常量分别是16,和0.75f,前者是初始化的数组长度。后者叫默认加载因子,可以理解为如果数组填充的长度到达默认长度的3/4就进行扩容。查看带参的构造器,一个叫initialCapacity,一个叫loadFactor。上来①先判断initialCapacity是否小于零,如果你new的时候写的initialCapacity非法就会报 IllegalArgumentException 异常。②判断initialCapacity是否大于系统可以创建的最大值MAXIMUM_CAPACITY这个值叫做最大支持容量为(2^30)如果比这个数还大就给initialCapacity赋值为2^30.③判断loadFactor是否小于等于零,或者Float.isNaN(loadFactor),这个isNaN(loadFactor)方法的作用就是判断loadFactor是否是非数字的值(NAN),NAN 是Not a Number的缩写。是数字的话返回flase,不是数字则返回ture,这两个条件是或的关系,有一个不成立就报IllegalArgumentException异常。如果上面的①②③都不满足的话就给initialCapacity和loadFactor赋new的值。赋值成功后的下一步就有意思了,你new的HashMap(15, 0.75f),实际上是initialCapacity是16,原因就是,在这步会定义一个int capacity=1;人后让while(capacity < initialCapacity){capacity<<=1 }即只要capacity < initialCapacity,capacity就一直乘以2,15的话就是16,25的话就是32依次类似,所以 initialCapacity都是2的多少次幂,且最小为16。而在这个死循环中就直接刚才new的loadFactor赋值给this.loadFactor。下一步给threshold(临界值)赋值,依据就是在capacity*loadFactor和MAXIMUM_CAPACITY+1中取最小值,一般情况下都是前者。下一步就new Entry([capacity ])叫table的数组。HashMap的底层就是一个Entry< k,v >类型的数组。至此数组就造好了。


添加元素的过程:调用put(K key,V value)方法。查看put(K key,V value)源码。上来首先判断key是否是null,如果是的话return putForNullKey(value)此方法专门处理null的value。下一步进行计算key的哈希值,得到哈希值后,下一步就是确定put的元素在数组的位置,调用indexOf方法将key的哈希值和table的length作为参数传入。这个方法return h & (length-1);得到的就是在数组的位置。解释一下,h就是哈希值,length-1就是数组的长度减一。可以理解为某个随机的数与15取模得到的可能是0-15的任何数,只是这个方法中使用的是 & 即与符号,这种方式效率更高。在此举例,如果这个数据key1得到的哈希值值是15(二进制与计算同一为一,否则为0),15(1111)&15(1111)得0000十进制就是0,则这个数据存在table[ 0 ]位置,如果key2得哈希值为31即31(11111) & 15(1111)得1111十进制就是15,则这个数据存储在table[ 15 ]位置。本质与15取模一致,就是通过位运算符计算得话效率高。得到具体的存放位置后,下面进行for循环源码显示for (Entry< K,V > e = table[ i ];e !=null ;e = e.next),这段代码解释就是首先看索引位置时候否已经有元素,没有得话就可以直接赋值,把元素放在这个位置上源码显示:addEntry(hash,key,value,i)。有元素的话进入for循环,当然有元素的话可能也不只一个(因为存的是链表),for循环内部做的工作是①判断已经在这个位置上的元素的哈希值是否与新添加的元素的哈希值相同。②判断old的key与new的key是否相等。如果相等循环内部做一个覆盖的操作。也就是,用新的key的value替换old的key的value(此部分不好解释。但看源码的话可以理解)。下面解释添加元素的操作即addEntry(hash,key,value,i)。这个i就是要存放的位置。这个方法内部源码显示做判断,看size?>threshold(12)&&null !=table[i = bucketIndex],这段解释就是,看现在数组已有的元素的长度是否大于临界值了,和看这个元素的位置是否为空。在理解为:就是看这个位置是不是空,如果用有元素就会形成链表就需要扩容了。没有元素就放在这里。所以扩容的条件可以说是,数组的现有元素数量比临界值还大且这个位置已经有元素。满足扩容条件的话就会resize数组的长度,源码显示:resize(2*table.length)即是扩容为原来的2倍。前面的if判断主要就是判断是否扩容,如果不需要扩容。做createEntry(hash,key,value,bucketIndex)操作。对于这个方法,源码显示上来首先把原有位置上的元素取出定义为e,源码显示:Entry < K,V > e = table [ bucketIndex ]。新的元素放在数组的这个位置上,原来的元素作为新元素的next属性出现。源码显示:table[ bucketIndex ] =new Entry<>(hash,key,value,e);简单理解为:旧元素指向新元素的next。下一步进行size++。至此添加元素的操作完成。

个人理解可能还是有偏差的,可以查看尚硅谷宋红康老师的详细的讲解总结:个人理解,有错误的请指教

个人理解:如果理解了JDK1.8的ArrayList源码,HashMap的理解会更加透彻些,实例化的方式都是new+构造器。调用空参的构造器,上来只做一个操作:this.loadFactor = DEFAULT_LOAD_FACTOR;把加载因子赋值为默认的即为0.75f,加载因子的作用前面已经讲到了。我们发现在空参的构造器中并没有像1.7里面那样给一个默认的数组长度16。在jdk1.8中底层的元素不叫Entry了,叫做Node,本质基本没有区别。在1.8里面Node实现了Map.Entry。本质上还是Entry。至此实例化就完成了。


添加元素的过程:调用put(K key,V value)方法,这个方法return putVal(hash(key), key, value, false, true);hash(key)本质就是获取key的哈希值。后两者:onlyIfAbsent – if true, don’t change existing value;evict – if false, the table is in creation mode.putVal内部:①判断table是否是null,是的话进入,把table给resize。首次调用会进入。resize内部上来首先把table给oldTable,然后判断oldTable是否是null,是的话把0赋值给oldCap,把临界值给oldThr,临界值这里暂时是0。下一步定义两个变量,newCap,newThr都是0.下一步两个判断oldCap > 0和oldThr > 0,显然都不满足。执行else内部:newCap = DEFAULT_INITIAL_CAPACITY(注:把初始化的数组长度给newCap);newThr = (int)(DEFAULT_LOAD_FACTOR DEFAULT_INITIAL_CAPACITY)(注:求出临界值,即0.7516=12);下一步继续进行判断newThr == 0显然不是不进去。下一步threshold = newThr(注:给临界值赋值);下一步就是new一个 newTable=Node[ newCap ],newCap=16,把new的数组给table即table=newTable,返回newTable。至此数组的创建就完成了②判断put的元素在数组的位置。把这个位置的值赋值给p,看p是不是null,是的话就直接存。不是null的话就进入else,else其中的步骤比较复杂。这里大概说在else里面都做了什么。首先做的就是判断新添加的元素与原位置的元素的key的哈希值是否相等。当然原位置可能是一个元素也可能是一个链表。也可能是一个红黑树。所以新添加的元素就不能只和一个元素比,在这个位置的所有元素的哈希值都需要比较。这个时候如果第一次比完就发现新添加的元素的哈希值和旧元素的哈希值一样就做替换(这里的替换其实还没有替换,只是做了赋值。下一步才是替换)。把新的元素放在旧的位置上。反应到实际中就是如果你往数组put相同的key但是value不同的值的时候,后者会覆盖前者。下一步会做判断if (e != null),这一步才是真正的替换。前面说了和一个元素,和一个链表,一个红黑数比较吗(if(一个)…else if(红黑树)…else(链表))。所以每个内部的方式都不一样,都需要写不同的判断逻辑。这里有个特点,就是,如果链表的长度=8的时候就会把这个链表变为红黑树,也不是等于8就变红黑树,在这个里面还会判断,当前数组的长度的是否大于64,如果大于则转为红黑树,小于64的话去做扩容(resize),不做转为树。下面一步就是转为树的操作了。

个人理解可能还是有偏差的,可以查看尚硅谷宋红康老师的详细的讲解总结:

点击图片可以跳转宋老师讲解


  • Map实现类之二:LinkedHashMap

    • LinkedHashMap 是 HashMap 的子类
    • 在HashMap存储结构的基础上,使用了一对双向链表来记录添加 元素的顺序
    • 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代 顺序:迭代顺序与 Key-Value 对的插入顺序一致
  • Map接口

LinkedHashMap是HashMap的子类,看是来是有序的。通过某种手段展出出有序的状态,可以和,put的时候是调用父类的put方法。LinkedHashMap里面重写了newHashMap方法。这个方法内部new了一个LinkedHashMap.Entry,这里的Entry继承了HashMap.Node,继承的同时又多定义了两个变量,分别是before和after,记录前一个和后一个。所以就可以按照顺序来遍历。Set里面是一个个的元素,这一个一个元素相当于Map里面的Key。value是PRESENT源码显示是new Object().其实没有实际的意义。


  • Map实现类之三:TreeMap
    • TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。
    • TreeMap 可以保证所有的 Key-Value 对处于有序状态。
    • TreeSet底层使用红黑树结构存储数据
    • TreeMap 的 Key 的排序:
      • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有 的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
      • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
    • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
      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
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      import org.junit.Test;

      import java.util.*;

      public class TreeMapTest {

      @Test
      public void NaturalOrderTest() {
      TreeMap treeMap = new TreeMap();

      Person p1 = new Person("Jermyn", 18);
      Person p2 = new Person("Jack", 20);
      Person p3 = new Person("Jerry", 17);
      Person p4 = new Person("Jackson", 23);

      treeMap.put(p1, 90);
      treeMap.put(p2, 85);
      treeMap.put(p3, 85);
      treeMap.put(p4, 95);

      Set entrySet = treeMap.entrySet();
      Iterator iterator = entrySet.iterator();
      while (iterator.hasNext()) {
      System.out.println(iterator.next());
      }
      }

      @Test
      public void CustomSortingTest() {

      TreeMap treeMap = new TreeMap(new Comparator() {
      @Override
      public int compare(Object o1, Object o2) {

      if (o1 instanceof Person && o2 instanceof Person) {
      Person p1 = (Person) o1;
      Person p2 = (Person) o2;
      return Integer.compare(p1.getAge(), p2.getAge());
      }
      throw new RuntimeException("输入类型不一致");
      }
      });

      Person p1 = new Person("Jermyn", 18);
      Person p2 = new Person("Jack", 20);
      Person p3 = new Person("Jerry", 17);
      Person p4 = new Person("Jackson", 23);
      treeMap.put(p1, 90);
      treeMap.put(p2, 85);
      treeMap.put(p3, 85);
      treeMap.put(p4, 95);

      Set entrySet = treeMap.entrySet();
      Iterator iterator = entrySet.iterator();
      while (iterator.hasNext()) {
      System.out.println(iterator.next());
      }
      }

      }

      class Person implements Comparable {

      private String name;
      private int age;

      public Person() {

      }

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

      public String getName() {
      return this.name;
      }

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

      public int getAge() {
      return this.age;
      }

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

      @Override
      public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      Person person = (Person) o;
      return age == person.age && Objects.equals(name, person.name);
      }

      @Override
      public int hashCode() {
      return Objects.hash(name, age);
      }

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

      @Override
      public int compareTo(Object o) {
      if (o instanceof Person) {
      Person p = (Person) o;
      int compare = this.name.compareTo(p.name);
      if (compare != 0) {
      return compare;
      } else {
      return Integer.compare(this.age, p.age);
      }
      } else {
      throw new RuntimeException("输入类型不一致");
      }
      }
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      D:\PATH-EN\java-path\bin\java.exe ...
      Key:name='Jerry', age=17 Value:=85
      Key:name='Jermyn', age=18 Value:=90
      Key:name='Jack', age=20 Value:=85
      Key:name='Jackson', age=23 Value:=95
      Key:name='Jack', age=20 Value:=85
      Key:name='Jackson', age=23 Value:=95
      Key:name='Jermyn', age=18 Value:=90
      Key:name='Jerry', age=17 Value:=85

      Process finished with exit code 0

  • Map实现类之四:Hashtable
    • Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,
    • Hashtable是线程安全的。
    • Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询 速度快,很多情况下可以互用。
    • 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
    • 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
    • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

  • Map实现类之五:Properties
    • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
    • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
    • 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
      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
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.util.Properties;

      public class PropertiesTest {

      public static void main(String[] args) throws Exception {
      FileInputStream fis = null;

      try {
      // key和value都是String
      Properties properties = new Properties();

      fis = new FileInputStream("jdbc.properties");

      properties.load(fis);

      String name = properties.getProperty("name");
      String password = properties.getProperty("password");

      System.out.println("name = " + name + ", password = " + password);
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      if (fis != null) {
      try {
      fis.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      }
      }
      }

      点击查看运行结果
      1
      2
      3
      4
      D:\PATH-EN\java-path\bin\java.exe ...
      name = admin管理员, password = 123456789

      Process finished with exit code 0
      1
      2
      name=admin管理员
      password=123456789

Collections工具类

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

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

    • 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 对象的所有旧值
  • Collections常用方法:同步控制
    Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集 合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    import org.testng.annotations.Test;

    import java.util.*;

    public class CollectionsTest {

    @Test
    public void SortRelatedTest() {

    ArrayList list = new ArrayList();
    list.add(26);
    list.add(12);
    list.add(20);
    list.add(70);
    list.add(20);
    list.add(12);
    list.add(65);
    list.add(54);
    list.add(12);

    System.out.println("original:" + list);

    Collections.shuffle(list);
    System.out.println("shuffle:" + list);

    Collections.reverse(list);
    System.out.println("reverse:" + list);

    Collections.sort(list);
    System.out.println("sort:" + list);

    Collections.swap(list, 1, 3);
    System.out.println("swap[index,index]:" + list);
    }

    @Test
    public void FindModifyTest() {

    ArrayList list = new ArrayList();
    list.add(26);
    list.add(12);
    list.add(20);
    list.add(21);
    list.add(12);

    System.out.println("original:" + list);

    Comparable max = Collections.max(list);
    System.out.println("maxOfList:" + max);

    int frequency = Collections.frequency(list, 12);
    System.out.println("frequencyOfElement(12)InList:" + frequency);
    }

    @Test
    public void CopyTest() {
    List list = new ArrayList();
    list.add(26);
    list.add(12);
    list.add(20);
    list.add(21);
    list.add(12);

    System.out.println("originalList:" + list);

    List destination = Arrays.asList(new Object[list.size()]);

    Collections.copy(destination, list);
    System.out.println("CopyListAsDest:" + destination);
    }

    @Test
    public void SyncTest() {
    List list = new ArrayList();
    list.add(26);

    List list1 = Collections.synchronizedList(list);
    System.out.println("经Collections.synchronizedList返回的list1是线程安全的");

    HashMap hashMap = new HashMap();
    hashMap.put("age", 18);

    Map map = Collections.synchronizedMap(hashMap);
    System.out.println("经Collections.synchronizedMap返回的map是线程安全的");
    }
    }

    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    D:\PATH-EN\java-path\bin\java.exe ...
    originalList:[26, 12, 20, 21, 12]
    CopyListAsDest:[26, 12, 20, 21, 12]
    original:[26, 12, 20, 21, 12]
    maxOfList:26
    frequencyOfElement(12)InList:2
    original:[26, 12, 20, 70, 20, 12, 65, 54, 12]
    shuffle:[65, 54, 20, 26, 12, 12, 70, 12, 20]
    reverse:[20, 12, 70, 12, 12, 26, 20, 54, 65]
    sort:[12, 12, 12, 20, 20, 26, 54, 65, 70]
    swap[index,index]:[12, 20, 12, 12, 20, 26, 54, 65, 70]
    经Collections.synchronizedList返回的list1是线程安全的
    经Collections.synchronizedMap返回的map是线程安全的

    ===============================================
    Default Suite
    Total tests run: 4, Passes: 4, Failures: 0, Skips: 0
    ===============================================


    Process finished with exit code 0

第五章 泛型

为什么要有泛型

  • 泛型的设计背景
    集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的 对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来 解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于 这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个 参数,这个类型参数叫做泛型。Collection,List,ArrayList这个就 是类型参数,即泛型。
  • 泛型的概念
    • 所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类 型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如, 继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实 际的类型参数,也称为类型实参)。
    • 从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念, 允许我们在创建集合时再指定集合元素的类型,正如:List,这表明 该List只能保存字符串类型的对象。
    • JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持, 从而可以在声明集合变量、创建集合对象时传入类型实参。

在集合中使用泛型

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
import org.testng.annotations.Test;

import java.util.*;

public class GenericTest {

@Test
public void GenericTest() {

ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(12);
arrayList.add(20);
arrayList.add(18);

// 遍历一:
for (Integer integer : arrayList) {
System.out.println(integer);
}

// 遍历二:
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}

@Test
public void HashMapTest() {
HashMap<String, Integer> HashMap = new HashMap<>();
HashMap.put("Jermyn", 18);
HashMap.put("Jack", 20);
HashMap.put("Jerry", 23);

Set<Map.Entry<String, Integer>> entries = HashMap.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);
}

}
}

点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\PATH-EN\java-path\bin\java.exe ...
12
20
18
12
20
18
Jermyn-->18
Jack-->20
Jerry-->23

===============================================
Default Suite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================


Process finished with exit code 0

自定义泛型结构

  • 泛型的声明
    interface List和 class GenTest:其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。常用T表示,是Type的缩写。

  • 泛型的实例化:

    • 一定要在类名后面指定类型参数的值(类型)。如:
      • ListstrList = new ArrayList();
      • Iteratoriterator = customers.iterator();
    • T只能是类,不能用基本数据类型填充。但可以使用包装类填充
    • 把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想
  • 自定义泛型结构:泛型类、泛型接口

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:
  2. 泛型类的构造器如下:public GenericClass(){}。 错误的:public GenericClass(){}
  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
  4. 泛型不同的引用不能相互赋值。>尽管在编译时ArrayList和ArrayList是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。
  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价 于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
  7. jdk1.7,泛型的简化操作:ArrayListflist = new ArrayList<>();
  8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换。
  9. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
  10. 异常类不能是泛型的
  11. 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
  12. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现

      • 没有类型 擦除
      • 具体类型
    • 子类保留父类的泛型:泛型子类

      • 全部保留
      • 部分保留
  13. 结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型

  • 自定义泛型结构:泛型方法
    • 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型 方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
      泛型方法的格式:
    • 格式
      1
      2
      3
      [访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
      eg:
      public <E> List copyFromArrayToList(E[] arr) throws RuntimeException

      示例

      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
      import java.util.ArrayList;
      import java.util.List;

      public class GenericMethodTest {

      public static void main(String[] args) {

      // 泛型类的类型跟泛型方法的类型无关
      Generic<String> stringGeneric = new Generic<>();

      Integer[] intArr = {1, 6, 5, 4, 9, 8};

      List<Integer> list = stringGeneric.copyFromArrayToList(intArr);
      System.out.println(list);
      }
      }


      class Generic<E> {

      public <E> List copyFromArrayToList(E[] arr) throws RuntimeException{

      ArrayList<E> list = new ArrayList<>();

      for (E e : arr) {
      list.add(e);
      }
      return list;
      }
      }
      点击查看运行结果
      1
      2
      3
      4
      D:\PATH-EN\java-path\bin\java.exe ...
      [1, 6, 5, 4, 9, 8]

      Process finished with exit code 0

通配符的使用

  1. 使用类型通配符:?
    • 比如:List<?>,Map<?,?>
    • List<?>是List< String >、List< Object >等各种泛型List的父类。
  2. 读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型 是什么,它包含的都是Object。
  3. 写入list中的元素时,不行。因为我们不知道c的元素类型,我们不能向其中添加对象。
    • 唯一的例外是null,它是所有类型的成员。
  • 因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集 合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。
  • 唯一的例外的是null,它是所有类型的成员。
  • 另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object。

    1
    2
    Collection<?> c = new ArrayList<String>();  
    c.add(new Object()); // 编译时错误

    示例

    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
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.LinkedList;
    import java.util.List;

    public class WildcardTest {

    public static void main(String[] args) {

    List<Integer> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(2);
    list1.add(3);

    List<String> list2 = new LinkedList<>();
    list2.add("Jer");
    list2.add("Jac");
    list2.add("Jerry");

    // 泛型不一样,不可赋值
    // list1=list2;
    // ? 指的是list1和list2的公共的父类
    List<?> list = null;
    list = list2;
    Iterator<?> iterator = list.iterator();
    while (iterator.hasNext()) {
    Object next = iterator.next();
    System.out.println(next);
    }

    list = list1;
    Iterator<?> iterator1 = list.iterator();
    while (iterator1.hasNext()) {
    Object next = iterator1.next();
    System.out.println(next);
    }
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    D:\PATH-EN\java-path\bin\java.exe ...
    Jer
    Jac
    Jerry
    1
    2
    3

    Process finished with exit code 0
  • 通配符的使用:有限制的通配符

  1. <?>
  2. 允许所有泛型的引用调用
  3. 通配符指定上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
  4. 通配符指定下限super:使用时指定的类型不能小于操作的类,即>=
  5. 举例:
    • <? extends Number>(无穷小 , Number]只允许泛型为Number及Number子类的引用调用
    • <? super Number> [Number , 无穷大)只允许泛型为Number及Number父类的引用调用
    • <? extends Comparable>只允许泛型为实现Comparable接口的实现类的引用调用
      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
      import java.util.List;

      public class GenericTest {

      public static void main(String[] args) {

      List<? extends Person> list1 = null;
      List<? super Person> list2 = null;

      List<Person> list3 = null;
      List<Student> list4 = null;
      List<Object> list5 = null;

      list1 = list3;
      list1 = list4;

      // 此处不可以,extends可以理解为 <= Person 的类或者子类
      // list1 = list5;


      list2 = list3;
      // 此处不可以,super可以理解为 >= Person 的类或者父类
      // list2 = list4;
      list2 = list5;

      }
      }

      class Person {
      }

      class Student extends Person {
      }

第六章 IO流

File类的使用

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

    • File 类的使用:常用构造器
    • public File(String pathname)—>以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。
    • 绝对路径:是一个固定的路径,从盘符开始
    • 相对路径:是相对于某个位置开始

    • public File(String parent,String child)—>以parent为父路径,child为子路径创建File对象。

    • public File(File parent,String child)—>根据一个父File对象和子文件路径创建File对象
  • File 类的使用:路径分隔符

    • 路径中的每级目录之间用一个路径分隔符隔开。
    • 路径分隔符和系统有关:

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

    • 为了解决这个隐患,File类提供了一个常量:public static final String separator。根据操作系统,动态的提供分隔符。
    • 举例:
      1
      2
      3
      File file1 = new File("d:\\jermyn\\info.txt");
      File file2 = new File("d:" + File.separator + "jermyn" + File.separator + "demo.txt");
      File file3 = new File("d:/jermyn");
  • File 类的使用:常用方法

    • File类的获取功能

      • public String getAbsolutePath():获取绝对路径
      • public String getPath() :获取路径
      • public String getName() :获取名称
      • public String getParent():获取上层文件目录路径。若无,返回null
      • public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
      • public long lastModified() :获取最后一次的修改时间,毫秒值

      • public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组

      • public File[] listFiles() :获取指定目录下的所有文件或者文件目录的File数组
    • File类的重命名功能

      • public boolean renameTo(File dest):把文件重命名为指定的文件路径File类的判断功能
      • public boolean isDirectory():判断是否是文件目录
      • public boolean isFile() :判断是否是文件
      • public boolean exists() :判断是否存在
      • public boolean canRead() :判断是否可读
      • public boolean canWrite() :判断是否可写
      • public boolean isHidden() :判断是否隐藏
    • File类的创建功能

      • public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false
      • public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。 如果此文件目录的上层目录不存在,也不创建。
      • public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建
        注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目 路径下。
    • File类的删除功能

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

    • 删除注意事项:

      • 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import org.testng.annotations.Test;

import java.io.File;
import java.io.IOException;
import java.util.Date;

public class FileTest {

@Test
public void FileTest() {

File file1 = new File("hello.txt");
File file2 = new File("D:\\学习\\Study\\java\\JavaSenior\\day25\\src\\cn\\jermyn\\test06\\hello.txt");

System.out.println("文件的绝对路径:" + file1.getAbsolutePath());
System.out.println("文件的路径:" + file1.getPath());
System.out.println("文件的名称:" + file1.getName());
System.out.println("文件的上层文件目录路径:" + file1.getParent());
System.out.println("文件长度:" + file1.length());
System.out.println("最后一次的修改时间:" + new Date(file1.lastModified()));

System.out.println("-----------------分割线-----------------");

System.out.println("文件的绝对路径:" + file2.getAbsolutePath());
System.out.println("文件的路径:" + file2.getPath());
System.out.println("文件的名称:" + file2.getName());
System.out.println("文件的上层文件目录路径:" + file2.getParent());
System.out.println("文件长度:" + file2.length());
System.out.println("最后一次的修改时间:" + new Date(file2.lastModified()));
}

@Test
public void FolderTest() {

File file = new File("D:\\学习\\Study\\java\\JavaSenior");

String[] list = file.list();
// 不存在文件抛异常
for (String s : list) {
System.out.println(s);
}

System.out.println("-----------------分割线-----------------");

File[] files = file.listFiles();
for (File f : files) {
System.out.println(f);
}
}

@Test
public void RenameTest() {
File file = new File("C:\\Users\\Administrator\\Desktop\\this.txt");
File destFile = new File("C:\\Users\\Administrator\\Desktop\\IO\\dest.txt");

// 返回true,需要file在硬盘中是存在的,且destFile不能在硬盘中存在
boolean b = file.renameTo(destFile);
System.out.println(b);
}

@Test
public void EstimateTest() {
File file = new File("C:\\Users\\Administrator\\Desktop\\dest.txt");
File file1 = new File("C:\\Users\\Administrator\\Desktop\\IO\\");

System.out.println("file是一个文件夹:" + file.isDirectory() + "\tfile1是一个文件夹:" + file1.isDirectory());
System.out.println("file是一个文件:" + file.isFile() + "\tfile1是一个文件:" + file1.isFile());
System.out.println("file存在:" + file.exists() + "\tfile1存在:" + file1.exists());
System.out.println("file是否可读:" + file.canRead() + "\tfile1是否可读:" + file1.canRead());
System.out.println("file是否可写:" + file.canWrite() + "\tfile1是否可写:" + file1.canWrite());
System.out.println("file是否是隐藏文件:" + file.isHidden() + "\tfile1是否是隐藏文件:" + file1.isHidden());
}

@Test
public void CreateFileTest() throws IOException {
File file = new File("C:\\Users\\Administrator\\Desktop\\this.txt");

if (!file.exists()) {
boolean newFile = file.createNewFile();
if (newFile == true) {
System.out.println("创建成功");
} else {
System.out.println("创建失败");
}
} else {
System.out.println("文件已经存在");

boolean delete = file.delete();
System.out.println("文件已经删除");
}
}

@Test
public void CreateDirectoryTest() {

File file = new File("C:\\Users\\Administrator\\Desktop\\IO\\SUBIO1");

// 创建一级目录
if (file.mkdir() == true) {
System.out.println("创建成功");
}

File file1 = new File("C:\\Users\\Administrator\\Desktop\\IO\\SUBIO1\\SUBIO2\\SUBIO3");
// 创建多级目录
if (file1.mkdirs() == true) {
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
D:\PATH-EN\java-path\bin\java.exe ...
创建成功
创建成功
文件已经存在
文件已经删除
file是一个文件夹:false file1是一个文件夹:true
file是一个文件:false file1是一个文件:false
file存在:false file1存在:true
file是否可读:false file1是否可读:true
file是否可写:false file1是否可写:true
file是否是隐藏文件:false file1是否是隐藏文件:false
文件的绝对路径:D:\学习\Study\java\JavaSenior\day25\hello.txt
文件的路径:hello.txt
文件的名称:hello.txt
文件的上层文件目录路径:null
文件长度:0
最后一次的修改时间:Thu Jan 01 08:00:00 CST 1970
-----------------分割线-----------------
文件的绝对路径:D:\学习\Study\java\JavaSenior\day25\src\cn\jermyn\test06\hello.txt
文件的路径:D:\学习\Study\java\JavaSenior\day25\src\cn\jermyn\test06\hello.txt
文件的名称:hello.txt
文件的上层文件目录路径:D:\学习\Study\java\JavaSenior\day25\src\cn\jermyn\test06
文件长度:26
最后一次的修改时间:Sat Oct 22 20:42:45 CST 2022
.idea
day18
day19
day20
day21
day22
day23
day24
day25
JavaSenior.iml
jdbc.properties
out
src
-----------------分割线-----------------
D:\学习\Study\java\JavaSenior\.idea
D:\学习\Study\java\JavaSenior\day18
D:\学习\Study\java\JavaSenior\day19
D:\学习\Study\java\JavaSenior\day20
D:\学习\Study\java\JavaSenior\day21
D:\学习\Study\java\JavaSenior\day22
D:\学习\Study\java\JavaSenior\day23
D:\学习\Study\java\JavaSenior\day24
D:\学习\Study\java\JavaSenior\day25
D:\学习\Study\java\JavaSenior\JavaSenior.iml
D:\学习\Study\java\JavaSenior\jdbc.properties
D:\学习\Study\java\JavaSenior\out
D:\学习\Study\java\JavaSenior\src
false

===============================================
Default Suite
Total tests run: 6, Passes: 6, Failures: 0, Skips: 0
===============================================


Process finished with exit code 0

IO流原理及流的分类

  • Java IO原理

    • I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。
    • Java程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行。
    • java.io包下提供了各种“流”类和接口,用以获取不同种类的 数据,并通过标准的方法输入或输出数据。
    • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
    • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。
  • 流的分类

    • 按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)
    • 按数据流的流向不同分为:输入流,输出流
    • 按流的角色的不同分为:节点流,处理流
      抽象基类节点流(文件流)缓冲流
      InputStreamFileInputStream (read(byte[] buffer))BufferedInputStream (read(byte[] buffer))
      OutputStreamFileOutputStream (write(byte[] buffer,0,len)BufferedOutputStream (write(byte[] buffer,0,len) / flush()
      ReaderFileReader (read(char[] cbuf))BufferedReader (read(char[] cbuf) / readLine())
      WriterFileWriter (write(char[] cbuf,0,len)BufferedWriter (write(char[] cbuf,0,len) / flush()
  • InputStream & Reader

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

      • int read()
      • int read(byte[] b)
      • int read(byte[] b, int off, int len)
    • Reader(典型实现:FileReader)

      • int read()
      • int read(char [] c)
      • int read(char [] c, int off, int len)
    • 程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资 源,所以应该显式关闭文件 IO 资源。

    • FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader
  • 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“关闭此输入流并释放与该流关联的所有系统资源。
  • 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
      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
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      import org.testng.annotations.Test;

      import java.io.File;
      import java.io.FileReader;
      import java.io.IOException;

      public class FileReaderTest {

      @Test
      public void testFileReader() {
      FileReader fr = null;

      try {
      //1.实例化File类的对象,指明要操作的文件
      File fileR = new File("C:\\Users\\Administrator\\Desktop\\IO\\demo.txt");

      //2.提供具体的流
      fr = new FileReader(fileR);

      //3.数据的读入
      int date;
      while ((date = fr.read()) != -1) {
      System.out.print((char) date);
      }
      } catch (IOException e) {
      e.printStackTrace();
      } finally {

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

      @Test
      public void testFileReader01() {
      FileReader fr = null;

      try {
      //1.实例化File类的对象,指明要操作的文件
      File fileR = new File("C:\\Users\\Administrator\\Desktop\\IO\\demo.txt");

      //2.提供具体的流
      fr = new FileReader(fileR);

      //3.读入操作
      char[] charBuffer = new char[1024];
      int len;
      while ((len = fr.read(charBuffer)) != -1) {

      // 方式一:
      System.out.println();
      System.out.println("----------------方式一----------------");
      for (int i = 0; i <= len; i++) {
      System.out.print(charBuffer[i]);
      }

      System.out.println();
      System.out.println("----------------方式二----------------");
      // 方式二:
      String str = new String(charBuffer, 0, len);
      System.out.println(str);
      }

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

      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      D:\PATH-EN\java-path\bin\java.exe ...
      abcdefghiklmnopqrstuvwxyz
      ----------------方式一----------------
      abcdefghiklmnopqrstuvwxyz
      ----------------方式二----------------
      abcdefghiklmnopqrstuvwxyz

      ===============================================
      Default Suite
      Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
      ===============================================


      Process finished with exit code 0
  • OutputStream & Writer

    • OutputStream 和 Writer 也非常相似:

      • void write(int b/int c);
      • void write(byte[] b/char[] cbuf);
      • void write(byte[] b/char[] buff, int off, int len);
      • void flush();
      • void close(); 需要先刷新,再关闭此流
    • 因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组, 即以 String 对象作为参数

      • void write(String str);
      • void write(String str, int off, int len);
    • FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter

  • 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:关闭此输出流并释放与该流关联的所有系统资源。
  • 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
      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
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      import org.testng.annotations.Test;

      import java.io.File;
      import java.io.FileReader;
      import java.io.FileWriter;
      import java.io.IOException;

      public class FileWriterTest {
      @Test
      public void testFileWriter() {
      FileWriter fw = null;
      try {
      //1.提供File类的对象,指明写出到的文件
      File fileW = new File("C:\\Users\\Administrator\\Desktop\\IO\\dest.txt");

      //2.提供FileWriter的对象,用于数据的写出
      // append:true 理解为追加,false理解为覆盖
      fw = new FileWriter(fileW, true);

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

      }

      @Test
      public void testFileReaderWriter() {
      FileReader fr = null;
      FileWriter fw = null;

      try {
      //1.提供File类的对象,指明读入的文件
      File fileR = new File("C:\\Users\\Administrator\\Desktop\\IO\\this.txt");

      //1.1提供File类的对象,指明写出到的文件
      File fileW = new File("C:\\Users\\Administrator\\Desktop\\IO\\dest.txt");

      //2.创建输入流和输出流的对象
      fr = new FileReader(fileR);
      fw = new FileWriter(fileW);

      //3.数据的读入和写出操作
      char[] charsBuffer = new char[5];
      int len;
      while ((len = fr.read(charsBuffer)) != -1) {
      //每次写出len个字符
      fw.write(charsBuffer, 0, len);
      }
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      //4.关闭流资源

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

      try {
      if (fw != null) {
      fw.close();
      }
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      }
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      8
      9
      D:\PATH-EN\java-path\bin\java.exe ...

      ===============================================
      Default Suite
      Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
      ===============================================


      Process finished with exit code 0

节点流(或文件流)

  • 节点流(或文件流):注意点

    • 定义文件路径时,注意:可以用“/”或者“\”。
    • 在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文件将被覆盖。
    • 如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖, 在文件内容末尾追加内容。
    • 在读取文件时,必须保证该文件已存在,否则报异常。
    • 字节流操作字节,比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
    • 字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文 本文件。
  • 步骤

  1. 处理对象
  2. 创建流
  3. 具体处理流程
  4. 关闭流

    示例,创建处理文件的方法

    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
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;

    public class FileTest {

    public static void main(String[] args) {
    long start = System.currentTimeMillis();

    copyFile("C:\\Users\\Administrator\\Desktop\\this.mp4",
    "C:\\Users\\Administrator\\Desktop\\dest.mp4");

    long end = System.currentTimeMillis();

    System.out.println("花费的时间为:" + (end - start) + "ms");
    }

    public static void copyFile(String thisPath, String destPath) {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
    File fileR = new File(thisPath);
    File fileW = new File(destPath);

    fis = new FileInputStream(fileR);
    fos = new FileOutputStream(fileW);

    int len;
    byte[] bytes = new byte[1024];
    while ((len = fis.read(bytes)) != -1) {
    fos.write(bytes, 0, len);
    }
    System.out.println("复制成功");
    } catch (IOException e) {
    e.printStackTrace();
    } finally {

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

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


    点击查看运行结果
    1
    2
    3
    4
    5
    D:\PATH-EN\java-path\bin\java.exe ...
    复制成功
    花费的时间为:42ms

    Process finished with exit code 0

    注:处理文本文件,例如txt,.java,.py等的使用FileReader,FileWriter,处理字节流的如.mp3,mp4等的,使用FileOutputStream,FileInputStream,处理文件。文本文件也可以用字节流处理,但是字节流不可以用文本处理方式。

缓冲流

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

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

    • BufferedInputStream 和 BufferedOutputStream
    • BufferedReader 和 BufferedWriter
  • 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区

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

    示例:BufferedReader和BufferedWriter

    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
    import java.io.*;

    public class BufferedReaderBufferedWriterTest {

    public static void main(String[] args) {

    BufferedReader br = null;
    BufferedWriter bw = null;
    try {
    br = new BufferedReader(
    new FileReader(
    new File("C:\\Users\\Administrator\\Desktop\\this.txt")));
    bw = new BufferedWriter(
    new FileWriter(
    new File("C:\\Users\\Administrator\\Desktop\\dest.txt")));

    String date;
    while ((date = br.readLine()) != null) {
    bw.write(date);
    bw.newLine();
    }
    } 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();
    }
    }
    }
    }
    }

    点击查看运行结果
    1
    2
    3
    D:\PATH-EN\java-path\bin\java.exe ...

    Process finished with exit code 0

    练习:获取文本上每个字符出现的次数

    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    import java.io.BufferedWriter;
    import java.io.FileReader;
    import java.io.FileWriter;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;


    public class WordCountTest {

    public static void main(String[] args) {
    testWordCount();
    }

    public static void testWordCount() {
    FileReader fr = null;
    BufferedWriter bw = null;
    try {
    //1.创建Map集合
    Map<Character, Integer> map = new HashMap<Character, Integer>();

    //2.遍历每一个字符,每一个字符出现的次数放到map中
    fr = new FileReader("C:\\Users\\Administrator\\Desktop\\this.txt");
    int c = 0;
    while ((c = fr.read()) != -1) {
    //int 还原 char
    char ch = (char) c;
    // 判断char是否在map中第一次出现
    if (map.get(ch) == null) {
    map.put(ch, 1);
    } else {
    map.put(ch, map.get(ch) + 1);
    }
    }

    //3.把map中数据存在文件count.txt
    //3.1 创建Writer
    bw = new BufferedWriter(new FileWriter("C:\\Users\\Administrator\\Desktop\\wordcount.txt"));

    //3.2 遍历map,再写入数据
    Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
    for (Map.Entry<Character, Integer> entry : entrySet) {
    switch (entry.getKey()) {
    case ' ':
    bw.write("空格=" + entry.getValue());
    break;
    case '\t'://\t表示tab 键字符
    bw.write("tab键=" + entry.getValue());
    break;
    case '\r'://
    bw.write("回车=" + entry.getValue());
    break;
    case '\n'://
    bw.write("换行=" + entry.getValue());
    break;
    default:
    bw.write(entry.getKey() + "=" + entry.getValue());
    break;
    }
    bw.newLine();
    }
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    //4.关流
    if (fr != null) {
    try {
    fr.close();
    } catch (IOException e) {
    e.printStackTrace();
    }

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

    点击查看运行结果
    1
    2
    3
    D:\PATH-EN\java-path\bin\java.exe ...

    Process finished with exit code 0

转换流

  • 转换流提供了在字节流和字符流之间的转换
  • Java API提供了两个转换流:
    • InputStreamReader:将InputStream转换为Reader
    • OutputStreamWriter:将Writer转换为OutputStream
  • 字节流中的数据都是字符时,转成字符流操作更高效。
  • 很多时候我们使用转换流来处理文件乱码问题。实现编码和 解码的功能。
  • InputStreamReader

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

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

        示例:UTF-8转GBK

        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
        import java.io.*;

        public class OutputStreamWriterTest {

        public static void main(String[] args) throws IOException {
        testOutputStreamWriter();
        }

        public static void testOutputStreamWriter() {

        InputStreamReader isrU = null;
        OutputStreamWriter oswG = null;
        try {

        FileInputStream fisU = new FileInputStream(
        "C:\\Users\\Administrator\\Desktop\\this.txt");
        FileOutputStream fosG = new FileOutputStream(
        "C:\\Users\\Administrator\\Desktop\\dest.txt");


        isrU = new InputStreamReader(fisU, "UTF-8");
        oswG = new OutputStreamWriter(fosG, "GBK");

        char[] chars = new char[1024];
        int len;
        while ((len = isrU.read(chars)) != -1) {
        oswG.write(chars, 0, len);
        }
        System.out.println("操作成功");
        } catch (IOException e) {
        e.printStackTrace();
        } finally {
        if (isrU != null){
        try {
        isrU.close();
        } catch (IOException e) {
        e.printStackTrace();
        }
        }

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

        点击查看运行结果
        1
        2
        3
        4
        D:\PATH-EN\java-path\bin\java.exe ...
        操作成功

        Process finished with exit code 0

  • 字符编码

  • 编码表的由来

    • 计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识 别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。 这就是编码表。
  • 常见的编码表

    • ASCII:美国标准信息交换码。用一个字节的7位可以表示。
    • ISO8859-1:拉丁码表。欧洲码表。用一个字节的8位表示。
    • GB2312:中国的中文编码表。最多两个字节编码所有字符
    • GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
    • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的 字符码。所有的文字都用两个字节来表示。
    • UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
  • Unicode不完美,这里就有三个问题,一个是,我们已经知道,英文字母只用 一个字节表示就够了,第二个问题是如何才能区别Unicode和ASCII?计算机 怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果 和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节, 就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时 间内无法推广,直到互联网的出现。

  • 面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF- 8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的 编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
  • Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯 一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的 Unicode编码是UTF-8和UTF-16。

标准输入、输出流

  • System.in和System.out分别代表了系统标准的输入和输出设备
  • 默认输入设备是:键盘,输出设备是:显示器
  • System.in的类型是InputStream
  • System.out的类型是PrintStream,其是OutputStream的子类 FilterOutputStream 的子类
  • 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
    • public static void setIn(InputStream in)
    • public static void setOut(PrintStream out)

      示例

      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
      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStreamReader;

      public class SystemInOutTest {

      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();
      }

      }
      }
      }
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      D:\PATH-EN\java-path\bin\java.exe ...
      请输入字符串:
      123
      123
      请输入字符串:
      jermyn
      JERMYN
      请输入字符串:
      e
      程序结束

      Process finished with exit code 0

打印流

  • 实现将基本数据类型的数据格式转化为字符串输出
  • 打印流:PrintStream和PrintWriter
    • 提供了一系列重载的print()和println()方法,用于多种数据类型的输出
    • PrintStream和PrintWriter的输出不会抛出IOException异常
      PrintStream和PrintWriter有自动flush功能
    • PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。
    • 在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
    • System.out返回的是PrintStream的实例

      示例

      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
      import java.io.File;
      import java.io.FileNotFoundException;
      import java.io.FileOutputStream;
      import java.io.PrintStream;

      public class PrintStreamWriter {

      public static void main(String[] args) {
      PrintStream ps = null;
      try {
      FileOutputStream fos = new FileOutputStream(new File("C:\\Users\\Administrator\\Desktop\\this.txt"));
      // 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
      ps = new PrintStream(fos, true);
      if (ps != null) {// 把标准输出流(控制台输出)改成文件
      System.setOut(ps);
      }


      for (int i = 0; i <= 255; i++) { // 输出ASCII字符
      System.out.print((char) i);
      if (i % 50 == 0) { // 每50个数据一行
      System.out.println(); // 换行
      }
      }

      } catch (FileNotFoundException e) {
      e.printStackTrace();
      } finally {
      if (ps != null) {
      ps.close();
      }
      }
      }
      }

      点击查看运行结果
      1
      2
      3
      D:\PATH-EN\java-path\bin\java.exe ...

      Process finished with exit code 0

数据流

  • 为了方便地操作Java语言的基本数据类型和String的数据,可以使用数据流。
  • 数据流有两个类:(用于读取和写出基本数据类型、String类的数据)
    • DataInputStream 和 DataOutputStream
    • 分别“套接”在 InputStream 和 OutputStream 子类的流上
  • DataInputStream中的方法

    • boolean readBoolean()
    • char readChar()
    • double readDouble()
    • long readLong()
    • String readUTF()
    • byte readByte()
    • float readFloat()
    • short readShort()
    • int readInt()
    • void readFully(byte[] b)
  • DataOutputStream中的方法

    • 将上述的方法的read改为相应的write即可。

      示例

      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
      import java.io.*;

      public class DateStreamTest {

      public static void main(String[] args) {
      DataOutputStream dos = null;
      DataInputStream dis = null;
      try {
      dos = new DataOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\data.txt"));
      dis = new DataInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\data.txt"));

      dos.writeUTF("Jermyn");
      dos.flush();
      dos.writeInt(23);
      dos.flush();
      dos.writeBoolean(true);
      dos.flush();

      String name = dis.readUTF();
      int age = dis.readInt();
      boolean isMale = dis.readBoolean();

      System.out.println("name = " + name);
      System.out.println("age = " + age);
      System.out.println("isMale = " + isMale);
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      if (dos != null) {
      try {
      dos.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }

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


      }
      }
      点击查看运行结果
      1
      2
      3
      4
      5
      6
      D:\PATH-EN\java-path\bin\java.exe ...
      name = Jermyn
      age = 23
      isMale = true

      Process finished with exit code 0

对象流

  • ObjectInputStream和OjbectOutputSteam

    • 用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可 以把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)

  • 使用对象流序列化对象

  • 若某个类实现了 Serializable 接口,该类的对象就是可序列化的:
    1. 创建一个 ObjectOutputStream
    2. 调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象
    3. 注意写出一次,操作flush()一次
  • 反序列化

    1. 创建一个 ObjectInputStream
    2. 调用 readObject() 方法读取流中的对象
  • 强调:如果某个类的属性不是基本数据类型或 String 类型,而是另一个 引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化

    示例

    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    import java.io.*;

    public class ObjectInputOutputStreamTest {

    public static void main(String[] args) {

    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    try {
    // 序列化(Serialization)
    File file = new File("C:\\Users\\Administrator\\Desktop\\file.dat");
    FileOutputStream fos = new FileOutputStream(file);
    oos = new ObjectOutputStream(fos);
    // ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\file.dat"));
    oos.writeObject(new Person("Jermyn", 18));
    oos.flush();

    // 反序列化(Deserialization)
    ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\file.dat"));
    Object o = ois.readObject();
    Person p = (Person) o;
    System.out.println(p);

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

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

    }
    }
    }


    class Person implements Serializable{

    private static final long serialVersionUID = 475463534532L;
    private String name;
    private int age;

    public Person() {
    }

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

    public String getName() {
    return name;
    }

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

    public int getAge() {
    return age;
    }

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

    @Override
    public String toString() {
    return "Person{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }
    点击查看运行结果
    1
    2
    3
    4
    D:\PATH-EN\java-path\bin\java.exe ...
    Person{name='Jermyn', age=18}

    Process finished with exit code 0

随机存取文件流

  • RandomAccessFile 类

    • RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并 且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也 可以写。
    • RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意
      地方来读、写文件

      • 支持只访问文件的部分内容
      • 可以向已存在的文件后追加内容
    • RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针:

      • long getFilePointer():获取文件记录指针的当前位置
      • void seek(long pos):将文件记录指针定位到 pos 位置
  • RandomAccessFile 类

    • 构造器

      • public RandomAccessFile(File file, String mode)
      • public RandomAccessFile(String name, String mode)
    • 创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指 定 RandomAccessFile 的访问模式:

      • r: 以只读方式打开
      • rw:打开以便读取和写入
      • rwd:打开以便读取和写入;同步文件内容的更新
      • rws:打开以便读取和写入;同步文件内容和元数据的更新
        点击图片可以跳转源码
    • 如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的文件, 如果读取的文件不存在则会出现异常。 如果模式为rw读写。如果文件不 存在则会去创建文件,如果存在则不会创建。

      关于RandomAccessFile的三种使用方法

      常规方式

      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
      import java.io.File;
      import java.io.IOException;
      import java.io.RandomAccessFile;

      public class RandomAccessFileTest {

      public static void main(String[] args) {

      RandomAccessFile rafR = null;
      RandomAccessFile rafW = null;
      try {
      rafR = new RandomAccessFile(
      new File("C:\\Users\\Administrator\\Desktop\\this.mp4"), "r");

      rafW = new RandomAccessFile(
      new File("C:\\Users\\Administrator\\Desktop\\dest.mp4"), "rw");

      byte[] bytes = new byte[1024];
      int len;
      while ((len = rafR.read(bytes)) != -1) {
      rafW.write(bytes, 0, len);
      }
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      if (rafR != null){
      try {
      rafW.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      if (rafW != null){
      try {
      rafR.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      }
      }
      }

      点击查看运行结果
      1
      2
      3
      D:\PATH-EN\java-path\bin\java.exe ...

      Process finished with exit code 0

      覆盖文件的内容

      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
      import java.io.File;
      import java.io.IOException;
      import java.io.RandomAccessFile;

      public class RandomAccessFileCoverTest {

      public static void main(String[] args) {
      RandomAccessFile rafRW = null;
      try {
      rafRW = new RandomAccessFile(
      new File("C:\\Users\\Administrator\\Desktop\\this.txt"), "rw");

      rafRW.write("abcdefghijklmnopqrstuvwxyz".getBytes());

      rafRW.seek(2);
      rafRW.write("CDEF".getBytes());

      rafRW.seek(rafRW.length());
      rafRW.write("123456789".getBytes());
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      if (rafRW != null) {
      try {
      rafRW.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      }
      }
      }

      点击查看运行结果
      1
      2
      3
      D:\PATH-EN\java-path\bin\java.exe ...

      Process finished with exit code 0

      实现插入操作

      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
      import java.io.File;
      import java.io.IOException;
      import java.io.RandomAccessFile;

      /**
      * Package: cn.jermyn.test02
      * Description:seek位置插入数据dataD思路:(讲究一个字拼)
      记录seek到File.length的数据dataB,在seek位置插入需要插入的数据dataD,在dataD后面插入dataB
      * Author: Jermyn
      * Version: 0.0.1
      */
      public class RandomAccessFileInsertTest {

      public static void main(String[] args) {
      RandomAccessFile rafRW = null;
      try {

      File file = new File("C:\\Users\\Administrator\\Desktop\\this.txt");
      rafRW = new RandomAccessFile(file, "rw");

      rafRW.write("abcdefghijklmnopqrstuvwxyz".getBytes());

      rafRW.seek(2);

      StringBuilder builder = new StringBuilder((int) file.length());
      byte[] bytes = new byte[1024];
      int len;
      while ((len = rafRW.read(bytes)) != -1) {
      builder.append(new String(bytes), 0, len);
      }

      rafRW.seek(2);

      rafRW.write("CEDF".getBytes());

      rafRW.write(builder.toString().getBytes());
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      if (rafRW != null) {
      try {
      rafRW.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      }
      }
      }

      点击查看运行结果
      1
      2
      3
      D:\PATH-EN\java-path\bin\java.exe ...

      Process finished with exit code 0

第七章 网络编程

通信要素1:IP和端口号

  • IP 地址:InetAddress

    • 唯一的标识 Internet 上的计算机(通信实体)
    • 本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
    • IP地址分类方式1:IPV4 和 IPV6
    • IPV4:4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如192.168.0.1
    • IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示, 数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
    • IP地址分类方式2:公网地址(万维网使用)和私有地址(局域网使用)。192.168. 开头的就是私有址址,范围即为192.168.0.0—192.168.255.255,专门为组织机 构内部使用
    • 特点:不易记忆
  • 端口号标识正在计算机上运行的进程(程序)

  • 不同的进程有不同的端口号
  • 被规定为一个 16 位的整数 0~65535。
  • 端口分类:

    • 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP占用端口80,FTP占用端口21,Telnet占用端口23)
    • 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
    • 动态/私有端口:49152~65535。
  • 端口号与IP地址的组合得出一个网络套接字:Socket。

  • InetAddress类

    • Internet上的主机有两种方式表示地址:

      • 域名(hostName):www.jermyn.cn
      • IP 地址(hostAddress):76.76.21.123
    • InetAddress类主要表示IP地址,两个子类:Inet4Address、Inet6Address。

    • InetAddress 类对象含有一个Internet主机地址的域名和 IP 地址
    • 域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。 ———-域名解析
    • InetAddress类没有提供公共的构造器,而是提供了如下几个静态方法来获取InetAddress实例

      • public static InetAddress getLocalHost()
      • public static InetAddress getByName(String host)
    • InetAddress提供了如下几个常用的方法

      • public String getHostAddress():返回 IP 地址字符串(以文本表现形式)。
      • public String getHostName():获取此 IP 地址的主机名
      • public boolean isReachable(int timeout):测试是否可以达到该地址

通信要素2:网络协议

  • TCP/IP协议簇

    • 传输层协议中有两个非常重要的协议:

      • 传输控制协议TCP(Transmission Control Protocol)
      • 用户数据报协议UDP(User Datagram Protocol)。
    • TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。

      • IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信。
      • TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、IP层、传输层和应用层。
  • TCP 和 UDP

    • TCP协议:

      • 使用TCP协议前,须先建立TCP连接,形成传输数据通道
      • 传输前,采用“三次握手”方式,点对点通信,是可靠的
      • TCP协议进行通信的两个应用进程:客户端、服务端。
      • 在连接中可进行大数据量的传输
      • 传输完毕,需释放已建立的连接,效率低
    • UDP协议:

      • 将数据、源、目的封装成数据包,不需要建立连接
      • 每个数据报的大小限制在64K内
      • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
      • 可以广播发送
      • 发送数据结束时无需释放资源,开销小,速度快
  • Socket

    • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实 上的标准。
    • 网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标 识符套接字。
    • 通信的两端都要有Socket,是两台机器间通信的端点。
    • 网络通信其实就是Socket间的通信。
    • Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
    • 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
    • Socket分类:
      • 流套接字(stream socket):使用TCP提供可依赖的字节流服务
      • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
  • Socket类的常用构造器:

    • public Socket(InetAddress address,int port)创建一个流套接字并将其连接到指定IP 地址的指定端口号。
    • public Socket(String host,int port)创建一个流套接字并将其连接到指定主机上的指定端口号。
  • Socket类的常用方法:

    • public InputStream getInputStream()返回此套接字的输入流。可以用于接收网络消息
    • public OutputStream getOutputStream()返回此套接字的输出流。可以用于发送网络消息
    • public InetAddress getInetAddress()此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。
    • public InetAddress getLocalAddress()获取套接字绑定的本地地址。 即本端的IP地址
    • public int getPort()此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。
    • public int getLocalPort()返回此套接字绑定到的本地端口。 如果尚未绑定套接字,则返回 -1。即本端的 端口号。
    • public void close()关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接 或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream。
    • public void shutdownInput()如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将 返回EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
    • public void shutdownOutput()禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发 送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流, 则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。

TCP网络编程

  • 基于Socket的TCP编程
  • Java语言的基于套接字编程分为服务端编程和客户端编程

  • 客户端Socket的工作过程包含以下四个基本的步骤:

    1. 创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
    2. 打开连接到Socket 的输入/出流: 使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流,进行数据传输
    3. 按照一定的协议对Socket 进行读/写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
    4. 关闭 Socket:断开客户端到服务器的连接,释放线路
  • 客户端创建Socket对象

    • 客户端程序可以使用Socket类创建对象,创建的同时会自动向服务器方发起连 接。Socket的构造器是:
    • Socket(String host,int port)throws UnknownHostException,IOException:向服务器(域名是host。端口号为port)发起TCP连接,若成功,则创建Socket对象,否则抛出异常。
    • Socket(InetAddress address,int port)throws IOException:根据InetAddress对象所表示的 IP地址以及端口号port发起连接。
  • 客户端建立socketAtClient对象的过程就是向服务器发出套接字连接请求

  • 服务器程序的工作过程包含以下四个基本的步骤:
  1. 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口 上。用于监听客户端的请求。
  2. 调用 accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信 套接字对象。
  3. 调用 该Socket类对象的 getOutputStream() 和 getInputStream ():获取输出流和输入流,开始网络数据的发送和接收。
  4. 关闭ServerSocket和Socket对象:客户端访问结束,关闭通信套接字。
  • 服务器建立 ServerSocket 对象
    • ServerSocket 对象负责等待客户端请求建立套接字连接,类似邮局某个窗口 中的业务员。也就是说,服务器必须事先建立一个等待客户请求建立套接字 连接的ServerSocket对象。
    • 所谓“接收”客户的套接字请求,就是accept()方法会返回一个 Socket 对象

示例:客户端发送内容给服务端,服务端将内容打印到控制台上。


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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import org.testng.annotations.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
* Description:客户端发送内容给服务端,服务端将内容打印到控制台上。
* Author: Jermyn
*/
public class TcpTest {

@Test
public void testClient() {

Socket socket = null;
OutputStream outputStream = null;
try {
//1.创建Socket对象,指明服务器端的ip和端口号
InetAddress byName = InetAddress.getByName("127.0.0.1");
socket = new Socket(byName, 8888);

//2.获取一个输出流,用于输出数据
outputStream = socket.getOutputStream();

//3.写出数据的操作
outputStream.write("你好我是客户端".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.资源的关闭
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}

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

@Test
public void testServer() {

ServerSocket serverSocket = null;
Socket socket = null;
InputStream inputStream = null;
ByteArrayOutputStream baos = null;
try {
//1.创建服务器端的ServerSocket,指明自己的端口号
serverSocket = new ServerSocket(8888);

//2.调用accept()表示接收来自于客户端的socket
socket = serverSocket.accept();

//3.获取输入流
inputStream = socket.getInputStream();

//4.读取输入流中的数据
baos = new ByteArrayOutputStream();

byte[] bytes = new byte[5];
int len;
while ((len = inputStream.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}

System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");
System.out.println("内容为:" + baos.toString());

} catch (IOException e) {
e.printStackTrace();
} finally {
//5.关闭资源
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

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

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

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

点击查看运行结果
1
2
D:\PATH-EN\java-path\bin\java.exe ...

示例二:客户端发送文件给服务端,服务端将文件保存在本地。


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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import org.testng.annotations.Test;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
* Description:客户端发送文件给服务端,服务端将文件保存在本地。
* Author: Jermyn
*/
public class TcpTest02 {

@Test
public void testClient() {
OutputStream os = null;
BufferedInputStream bis = null;
Socket socket = null;
try {
// 造socket
socket = new Socket(InetAddress.getByName("127.0.0.1"), 8888);

// 获取输出流
os = socket.getOutputStream();

// 处理流
FileInputStream fis = new FileInputStream(
"C:\\Users\\Administrator\\Desktop\\Socket\\client\\this.mp4");

// 缓冲流
bis = new BufferedInputStream(fis);

// 读写过程
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//资源关闭
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}

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

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

@Test
public void testServer() {
ServerSocket serverSocket = null;
Socket accept = null;
InputStream is = null;
BufferedOutputStream bos = null;
try {
// 造客户端socket
serverSocket = new ServerSocket(8888);

// 造socket
accept = serverSocket.accept();

// 输入流
is = accept.getInputStream();

// 处理流
FileOutputStream fos = new FileOutputStream(
"C:\\Users\\Administrator\\Desktop\\Socket\\server\\dest.mp4");

// 缓冲流
bos = new BufferedOutputStream(fos);

// 处理过程
byte[] bytes = new byte[1024];
int len;
while ((len = is.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 资源关闭
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

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

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

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

点击查看运行结果
1
2
D:\PATH-EN\java-path\bin\java.exe ...

练习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
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import org.testng.annotations.Test;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
* Description:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给 客户端。并关闭相应的连接。
* Author: Jermyn
*/
public class TcpTest03 {
@Test
public void testClient() {
OutputStream os = null;
BufferedInputStream bis = null;
Socket socket = null;
ByteArrayOutputStream baos = null;
InputStream is = null;
try {
// 1.造socket
socket = new Socket(InetAddress.getByName("127.0.0.1"), 8888);

// 2.获取输出流
os = socket.getOutputStream();

// 3.处理流
FileInputStream fis = new FileInputStream(
"C:\\Users\\Administrator\\Desktop\\Socket\\client\\this.mp4");

// 4.缓冲流
bis = new BufferedInputStream(fis);

// 5.读写过程
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}

// 6.关闭数据的输出,表示停止数据传输
socket.shutdownOutput();

// 7.客户端接收服务端反馈信息
is = socket.getInputStream();
baos = new ByteArrayOutputStream();

byte[] bytes2 = new byte[20];
int length;
while ((length = is.read(bytes2)) != -1) {
baos.write(bytes2, 0, length);
}
System.out.println("收到了来自于服务端:" + socket.getInetAddress().getHostAddress() + "的反馈信息");
System.out.println("内容为:\"" + baos.toString() + "\"");

} catch (IOException e) {
e.printStackTrace();
} finally {
// 8.资源关闭
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}

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

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

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

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

@Test
public void testServer() {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream is = null;
BufferedOutputStream bos = null;
OutputStream os = null;
try {
// 1.造客户端socket
serverSocket = new ServerSocket(8888);

// 2.造socket
socket = serverSocket.accept();

// 3.输入流
is = socket.getInputStream();

// 4.处理流
FileOutputStream fos = new FileOutputStream(
"C:\\Users\\Administrator\\Desktop\\Socket\\server\\dest.mp4");

// 5.缓冲流
bos = new BufferedOutputStream(fos);

// 6.处理过程
byte[] bytes = new byte[1024];
int len;
while ((len = is.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
System.out.println("收到了来自于服务端:" + socket.getInetAddress().getHostAddress() + "的反馈数据");
System.out.println("传输数据成功");
System.out.println("发送反馈信息");

// 7.服务端给予客户端反馈
os = socket.getOutputStream();
os.write("你好客户端,数据我已收到".getBytes());

} catch (IOException e) {
e.printStackTrace();
} finally {
// 资源关闭
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

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

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

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

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

点击查看运行结果
1
2
D:\PATH-EN\java-path\bin\java.exe ...


UDP网络编程

  • UDP网络通信

    • 类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。
    • UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证
    • UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
    • DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP 地址和端口号以及接收端的IP地址和端口号。
    • UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和 接收方的连接。如同发快递包裹一样。
  • DatagramSocket 类的常用方法

    • public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。
    • public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。 本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地 址,IP 地址由内核选择。
    • public void close()关闭此数据报套接字。
    • public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
    • public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法 在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的 长度长,该信息将被截短。
    • public InetAddress getLocalAddress()获取套接字绑定的本地地址。
    • public int getLocalPort()返回此套接字绑定的本地主机上的端口号。
    • public InetAddress getInetAddress()返回此套接字连接的地址。如果套接字未连接,则返回null。
    • public int getPort()返回此套接字的端口。如果套接字未连接,则返回 -1。
    • public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为length 的数据包。 length 参数必须小于等于 buf.length。
    • public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数 据报包,用来将长度为 length的包发送到指定主机上的指定端口号。length 参数必须小于等于buf.length。
    • public InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该
      机器或者是从该机器接收到的。
    • public int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或 者是从该主机接收到的。
    • public byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续length 长度。
    • public int getLength()返回将要发送或接收到的数据的长度。
  • 流 程:

    1. DatagramSocket与DatagramPacket
    2. 建立发送端,接收端
    3. 建立数据包
    4. 调用Socket的发送、接收方法
    5. 关闭Socket

      示例

      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
      61
      62
      63
      64
      65
      66
      import org.testng.annotations.Test;

      import java.io.IOException;
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      import java.net.InetAddress;

      public class UdpTest {

      @Test
      public void testSender() {

      DatagramSocket socket = null;
      try {
      String str = new String("UDP发送数据");
      byte[] data = str.getBytes();

      InetAddress inet = InetAddress.getLocalHost();

      socket = new DatagramSocket();

      DatagramPacket packet = new DatagramPacket(data, 0, data.length, inet, 8888);

      socket.send(packet);
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      if (socket != null) {
      try {
      socket.close();
      } catch (Exception e) {
      e.printStackTrace();
      }
      }
      }

      }

      @Test
      public void testReceiver() throws IOException {

      DatagramSocket socket = null;
      try {
      socket = new DatagramSocket(8888);

      byte[] data = new byte[100];

      DatagramPacket packet = new DatagramPacket(data, 0, data.length);

      socket.receive(packet);

      System.out.println(new String(packet.getData(), 0, packet.getLength()));
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      if (socket != null) {
      try {
      socket.close();
      } catch (Exception e) {
      e.printStackTrace();
      }
      }
      }
      }
      }

      点击查看运行结果

URL编程

  • URL类

    • URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。
    • 它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
    • 通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。
    • URL的基本结构由5部分组成:

    • 为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初 始化一个 URL 对象:

      • public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例 如:URL url = new URL (“http://www. jermyn.cn/“);
      • public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。例如:URL downloadUrl = new URL(url, “download.html”)
      • public URL(String protocol, String host, String file); 例如:new URL(“http”, “www.jermyn.cn”, “download. html”);
      • public URL(String protocol, String host, int port, String file); 例如: URL gamelan = new
        URL(“http”, “www.jermyn.cn”, 80, “download.html”);
    • URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通 常是用 try-catch 语句进行捕获。

    • 一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的 方法来获取这些属性:

      • public String getProtocol ( ) 获取该 URL 的协议名
      • public String getHost ( ) 获取该URL的主机名
      • public String getPort ( ) 获取该URL的端口号
      • public String getPath ( ) 获取该URL的文件路径
      • public String getFile ( ) 获取该URL的文件名
      • public String getQuery ( ) 获取该URL的查询名
  • 针对HTTP协议的URLConnection类

    • URL的方法 openStream():能从网络上读取数据
    • 若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一 些数据,则必须先与URL建立连接,然后才能对其进行读写,此时需要使用 URLConnection 。
    • URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时, 首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection 对象。如果连接过程失败,将产生IOException.

    • 通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互。

      • public Object getContent( ) throws IOException
      • public int getContentLength( )
      • public String getContentType( )
      • public long getDate( )
      • public long getLastModified( )
      • public InputStream getInputStream( )throws IOException
      • public OutputSteram getOutputStream( )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
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        import java.io.FileOutputStream;
        import java.io.IOException;
        import java.io.InputStream;
        import java.net.HttpURLConnection;
        import java.net.URL;

        public class URLTest {

        public static void main(String[] args) {

        HttpURLConnection urlConnection = null;
        InputStream is = null;
        FileOutputStream fos = null;
        try {
        URL url = new URL("$URL");

        urlConnection = (HttpURLConnection) url.openConnection();

        urlConnection.connect();

        is = urlConnection.getInputStream();
        fos = new FileOutputStream("$DEST_PATH");

        byte[] buffer = new byte[1024];
        int len;
        while((len = is.read(buffer)) != -1){
        fos.write(buffer,0,len);
        }

        System.out.println("下载完成");
        } catch (IOException e) {
        e.printStackTrace();
        } finally {
        //关闭资源
        if(is != null){
        try {
        is.close();
        } catch (IOException e) {
        e.printStackTrace();
        }
        }
        if(fos != null){
        try {
        fos.close();
        } catch (IOException e) {
        e.printStackTrace();
        }
        }
        if(urlConnection != null){
        urlConnection.disconnect();
        }
        }
        }
        }

  • 小结

    • 位于网络中的计算机具有唯一的IP地址,这样不同的主机可以互相区分。
    • 客户端-服务器是一种最常见的网络应用程序模型。服务器是一个为其客户端提供某种特定 服务的硬件或软件。客户机是一个用户应用程序,用于访问某台服务器提供的服务。端口号 是对一个服务的访问场所,它用于区分同一物理计算机上的多个服务。套接字用于连接客户 端和服务器,客户端和服务器之间的每个通信会话使用一个不同的套接字。TCP协议用于实 现面向连接的会话。
    • Java 中有关网络方面的功能都定义在 java.net 程序包中。Java 用 InetAddress 对象表示 IP地址,该对象里有两个字段:主机名(String) 和 IP 地址(int)。
    • 类 Socket 和 ServerSocket 实现了基于TCP协议的客户端-服务器程序。Socket是客户端 和服务器之间的一个连接,连接创建的细节被隐藏了。这个连接提供了一个安全的数据传输 通道,这是因为 TCP 协议可以解决数据在传送过程中的丢失、损坏、重复、乱序以及网络 拥挤等问题,它保证数据可靠的传送。
    • 类 URL 和 URLConnection 提供了最高级网络应用。URL 的网络资源的位置来同一表示 Internet 上各种网络资源。通过URL对象可以创建当前应用程序和 URL 表示的网络资源之 间的连接,这样当前程序就可以读取网络资源数据,或者把自己的数据传送到网络上去。

第八章 Java反射机制

Java反射机制概述

  • Java Reflection

    • Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期 借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内 部属性及方法。
    • 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个 类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可 以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看 到类的结构,所以,我们形象的称之为:反射。
  • Java反射机制提供的功能

    • 在运行时判断任意一个对象所属的类
    • 在运行时构造任意一个类的对象
    • 在运行时判断任意一个类所具有的成员变量和方法
    • 在运行时获取泛型信息
    • 在运行时调用任意一个对象的成员变量和方法
    • 在运行时处理注解
    • 生成动态代理

理解Class类并获取Class的实例

  • Class 类

    • 在Object类中定义了以下的方法,此方法将被所有子类继承:

      • public final Class getClass()
    • 以上的方法返回值的类型是一个Class类, 此类是Java反射的源头,实际上所谓反射 从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。

    • 对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
      • Class本身也是一个类
      • Class 对象只能由系统建立对象
      • 一个加载的类在 JVM 中只会有一个Class实例
      • 一个Class对象对应的是一个加载到JVM中的一个.class文件
      • 每个类的实例都会记得自己是由哪个 Class 实例所生成
      • 通过Class可以完整地得到一个类中的所有被加载的结构
      • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的
      • Class对象
  • 获取Class类的实例(四种方法)

  1. 前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠, 程序性能最高
    实例:Class clazz = String.class;
  2. 前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象
    实例:Class clazz = “www.atguigu.com”.getClass();
  3. 前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方 法forName()获取,可能抛ClassNotFoundException
    实例:Class clazz = Class.forName(“java.lang.String”);
  4. 其他方式(不做要求)
    ClassLoader cl = this.getClass().getClassLoader();
    Class clazz4 = cl.loadClass(“类的全类名”);

    示例

    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
    import org.testng.annotations.Test;

    public class ReflectionTest {

    @Test
    public void testGetClassInstance() throws ClassNotFoundException {

    //方式一:调用运行时类的属性:.class
    Class clazz1 = Person.class;
    System.out.println(clazz1);

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

    //方式三:调用Class的静态方法:forName(String classPath)
    Class clazz3 = Class.forName("cn.jermyn.test01.Person");
    System.out.println(clazz3);

    //方式四:使用类的加载器:ClassLoader (了解)
    ClassLoader classLoader = ReflectionTest.class.getClassLoader();
    Class clazz4 = classLoader.loadClass("cn.jermyn.test01.Person");
    System.out.println(clazz4);
    }
    }

    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    D:\PATH-EN\java-path\bin\java.exe ...
    class cn.jermyn.test01.Person
    class cn.jermyn.test01.Person
    class cn.jermyn.test01.Person

    ===============================================
    Default Suite
    Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
    ===============================================


    Process finished with exit code 0
  • 哪些类型可以有Class对象?
    1. class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
    2. interface:接口
    3. []:数组
    4. enum:枚举
    5. annotation:注解@interface
    6. primitive type:基本数据类型
    7. void

类的加载与ClassLoader的理解

  • 类的加载过程
    当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过 如下三个步骤来对该类进行初始化。
    1. 类的加载(Load):将类的class文件读入内存,并为之创建一 个java.lang.Class对象。此过程由类加载器完成
    2. 类的链接(Link):将类的二进制数 据合并到JRE中
    3. 类的初始化(Initialize):JVM负责对类 进行初始化
  • 加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时 数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问 入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的 过程需要类加载器参与。
  • 链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。

    • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存 都将在方法区中进行分配。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  • 初始化:

    • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中 所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信 息的,不是构造该类对象的构造器)。
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
    • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
  • 什么时候会发生类初始化?

    • 类的主动引用(一定会发生类的初始化)

      • 当虚拟机启动,先初始化main方法所在的类
      • new一个类的对象
      • 调用类的静态成员(除了final常量)和静态方法
      • 使用java.lang.reflect包的方法对类进行反射调用
      • 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
    • 类的被动引用(不会发生类的初始化)

    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化
      • 当通过子类引用父类的静态变量,不会导致子类初始化
    • 通过数组定义类引用,不会触发此类的初始化
    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常 量池中了)
  • 类加载器的作用:

    • 类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方 法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为 方法区中类数据的访问入口。
    • 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器 中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

创建运行时类的对象

  • 有了Class对象,能做什么?

    • 创建类的对象:调用Class对象的newInstance()方法
    • 要 求:
      1. 类必须有一个无参数的构造器。
      2. 类的构造器的访问权限需要足够。
  • 难道没有无参的构造器就不能创建对象了吗?
    不是!只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。
    步骤如下:

    1. 通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类 型的构造器
    2. 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
    3. 通过Constructor实例化对象。

      示例

      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
      import org.testng.annotations.Test;

      import java.util.Random;

      public class NewInstanceTest {

      @Test
      public void testMethod() {

      for (int i = 0; i < 10; i++) {
      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 = "cn.jermyn.test01.Person";
      break;
      }

      try {
      Object obj = getInstance(classPath);
      System.out.println(obj);
      } catch (Exception e) {
      e.printStackTrace();
      }
      }
      }

      public Object getInstance(String classPath) throws Exception {
      Class clazz = Class.forName(classPath);
      return clazz.newInstance();
      }

      @Test
      public void testMethod02() throws ClassNotFoundException, InstantiationException, IllegalAccessException {


      Class<Person> aClass = (Class<Person>) Class.forName("cn.jermyn.test01.Person");

      Person person = aClass.newInstance();

      System.out.println(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
      D:\PATH-EN\java-path\bin\java.exe ...
      ----------------分割线----------------
      Thu Oct 27 20:31:57 CST 2022
      java.lang.Object@3abbfa04
      Person()
      Person{name='null', age=0}
      Person()
      Person{name='null', age=0}
      Thu Oct 27 20:31:57 CST 2022
      java.lang.Object@57fffcd7
      Thu Oct 27 20:31:57 CST 2022
      Thu Oct 27 20:31:57 CST 2022
      Thu Oct 27 20:31:57 CST 2022
      Thu Oct 27 20:31:57 CST 2022
      ----------------分割线----------------
      Person()
      Person{name='null', age=0}

      ===============================================
      Default Suite
      Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
      ===============================================


      Process finished with exit code 0

获取运行时类的完整结构

通过反射获取运行时类的完整结构Field、Method、Constructor、Superclass、Interface、Annotation
实现的全部接口;所继承的父类;全部的构造器;全部的方法;全部的Field;

  • 使用反射可以取得:

    1. 实现的全部接口

      • public Class<?>[] getInterfaces()
        确定此对象所表示的类或接口实现的接口。
    2. 所继承的父类

      • public Class<? Super T> getSuperclass()
        返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的Class。
    3. 全部的构造器

      • public Constructor[] getConstructors()
        返回此 Class 对象所表示的类的所有public构造方法。
      • public Constructor[] getDeclaredConstructors()
        此 Class 对象表示的类声明的所有构造方法。

      • Constructor类中:

        • 取得修饰符: public int getModifiers();
        • 取得方法名称: public String getName();
        • 取得参数的类型:public Class<?>[] getParameterTypes();
    4. 全部的方法

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

      • Method类中:

        • public Class<?> getReturnType()取得全部的返回值
        • public Class<?>[] getParameterTypes()取得全部的参数
        • public int getModifiers()取得修饰符
        • public Class<?>[] getExceptionTypes()取得异常信息
    5. 全部的Field

      • public Field[] getFields()
        返回此Class对象所表示的类或接口的public的Field。
      • public Field[] getDeclaredFields()
        返回此Class对象所表示的类或接口的全部Field。

      • Field方法中:

        • public int getModifiers()
          以整数形式返回此Field的修饰符
        • public Class<?> getType()
          得到Field的属性类型
        • public String getName()
          返回Field的名称。
    6. Annotation相关

      • get Annotation(ClassannotationClass)
      • getDeclaredAnnotations()
    7. 泛型相关

      • 获取父类泛型类型:Type getGenericSuperclass()
      • 泛型类型:ParameterizedType
      • 获取实际的泛型类型参数数组:getActualTypeArguments()
    8. 类所在的包

      • Package getPackage()

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.jermyn.test02;

import java.io.Serializable;


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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package cn.jermyn.test02;


@MyAnnotation(value = "hi")
public class Person extends Creature<String> implements Comparable<String>, MyInterface {

private String name;
int age;
public int id;

public Person() {
}

@MyAnnotation(value = "abc")
private Person(String name) {
this.name = name;
}

Person(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, int age) throws NullPointerException, ClassCastException {
return interests + age;
}


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

@Override
public int compareTo(String o) {
return 0;
}

private static void showDesc() {
System.out.println("我是一个可爱的人");
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.jermyn.test02;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;


@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "hello";

}
1
2
3
4
5
6
package cn.jermyn.test02;


public interface MyInterface {
void info();
}
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
package cn.jermyn.test02;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class FieldTest {

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

Class<Person> clazz = (Class<Person>) Class.forName("cn.jermyn.test02.Person");

// 获取属性结构
//getFields():获取当前运行时类及其父类中声明为public访问权限的属性
Field[] fields = clazz.getFields();
for (Field f : fields) {
System.out.println(f);
}

System.out.println("-----------------------------------");

//getDeclaredFields():获取当前运行时类中声明的所有属性。(不包含父类中声明的属性)
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields) {
System.out.println(f);
}

System.out.println("-----------------------------------");

// 权限修饰符 数据类型 变量名
for (Field f : declaredFields) {
//1.权限修饰符
int modifier = f.getModifiers();
System.out.print(modifier + ":" + Modifier.toString(modifier) + "\t\t");

//2.数据类型
Class type = f.getType();
System.out.print(type.getName() + "\t\t");

//3.变量名
String fName = f.getName();
System.out.print(fName);

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
61
62
63
64
65
66
67
package cn.jermyn.test02;

import org.testng.annotations.Test;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class MethodTest {

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

Class<Person> clazz = (Class<Person>) Class.forName("cn.jermyn.test02.Person");

//getMethods():获取当前运行时类及其所有父类中声明为public权限的方法
Method[] methods = clazz.getMethods();
for (Method m : methods) {
System.out.println(m);
}

System.out.println("-----------------------------------");

//getDeclaredMethods():获取当前运行时类中声明的所有方法。(不包含父类中声明的方法)
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.println(m);
}
}

@Test
public void testOthers() {

Class clazz = Person.class;
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
//1.获取方法声明的注解
Annotation[] annos = m.getAnnotations();
for (Annotation a : annos) {
System.out.println(a);
}
//2.权限修饰符
System.out.print(Modifier.toString(m.getModifiers()) + "\t");

//3.返回值类型
System.out.print(m.getReturnType().getName() + "\t");

//4.方法名
System.out.print(m.getName());
System.out.print("(");

//5.形参列表
Class[] parameterTypes = m.getParameterTypes();
if (!(parameterTypes == null && parameterTypes.length == 0)) {
for (int i = 0; i < parameterTypes.length; i++) {

if (i == parameterTypes.length - 1) {
System.out.print(parameterTypes[i].getName() + " args_" + i);
break;
}

System.out.print(parameterTypes[i].getName() + " args_" + i + ",");
}
}
}
}
}

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package cn.jermyn.test02;

import org.testng.annotations.Test;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class OtherTest {

/*
获取构造器结构
*/
@Test
public void test1() {

Class clazz = Person.class;
//getConstructors():获取当前运行时类中声明为public的构造器
Constructor[] constructors = clazz.getConstructors();
for (Constructor c : constructors) {
System.out.println(c);
}

System.out.println();
//getDeclaredConstructors():获取当前运行时类中声明的所有的构造器
Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
for (Constructor c : declaredConstructors) {
System.out.println(c);
}

}

/*
获取运行时类的父类
*/
@Test
public void test2() {
Class clazz = Person.class;

Class superclass = clazz.getSuperclass();
System.out.println(superclass);
}

/*
获取运行时类的带泛型的父类
*/
@Test
public void test3() {
Class clazz = Person.class;

Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println(genericSuperclass);
}

/*
获取运行时类的带泛型的父类的泛型
*/
@Test
public void test4() {
Class clazz = Person.class;

Type genericSuperclass = clazz.getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) genericSuperclass;
//获取泛型类型
Type[] actualTypeArguments = paramType.getActualTypeArguments();
// System.out.println(actualTypeArguments[0].getTypeName());
System.out.println(((Class) actualTypeArguments[0]).getName());
}

/*
获取运行时类实现的接口
*/
@Test
public void test5() {
Class clazz = Person.class;

Class[] interfaces = clazz.getInterfaces();
for (Class c : interfaces) {
System.out.println(c);
}

System.out.println();
//获取运行时类的父类实现的接口
Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
for (Class c : interfaces1) {
System.out.println(c);
}

}

/*
获取运行时类所在的包
*/
@Test
public void test6() {
Class clazz = Person.class;

Package pack = clazz.getPackage();
System.out.println(pack);
}

/*
获取运行时类声明的注解
*/
@Test
public void test7() {
Class clazz = Person.class;

Annotation[] annotations = clazz.getAnnotations();
for (Annotation annos : annotations) {
System.out.println(annos);
}
}
}

调用运行时类的指定结构

  • 调用指定方法
    通过反射,调用类中的方法,通过Method类完成。步骤:
  1. 通过Class类的getMethod(String name,Class…parameterTypes)方法取得一个Method对象,并设置此方法操作时所需要的参数类型。
  2. 之后使用Object invoke(Object obj, Object[] args)进行调用,并向方法中传递要设置的obj对象的参数信息。
  • Object invoke(Object obj, Object … args)说明:

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

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

      • public Field getField(String name) 返回此Class对象表示的类或接口的指定的public的Field。
      • public Field getDeclaredField(String name)返回此Class对象表示的类或接口的 指定的Field。
    • 在Field中:

      • public Object get(Object obj) 取得指定对象obj上此Field的属性内容
      • public void set(Object obj,Object value) 设置指定对象obj上此Field的属性内容
  • 关于setAccessible方法的使用

    • Method和Field、Constructor对象都有setAccessible()方法。
    • setAccessible启动和禁用访问安全检查的开关。
    • 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
      • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。
      • 使得原本无法访问的私有成员也可以访问
    • 参数值为false则指示反射的对象应该实施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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import cn.jermyn.test01.Person;
import org.testng.annotations.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionTest {

@Test
public void test01() throws InstantiationException, IllegalAccessException, NoSuchFieldException, ClassNotFoundException {

Class clazz = Person.class;

//创建运行时类的对象
Person p = (Person) clazz.newInstance();

//获取指定的属性:要求运行时类中属性声明为public
//通常不采用此方法
Field id = clazz.getField("id");


// 设置当前属性的值 set(参数1,参数2):参数1:指明设置哪个对象的属性 参数2:将此属性值设置为多少
id.set(p, 1001);

// 获取当前属性的值get():参数1:获取哪个对象的当前属性值
int pId = (int) id.get(p);
System.out.println(pId);
}

// 调属性
@Test
public void test02() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException {

Class<Person> clazz = (Class<Person>) Class.forName("cn.jermyn.test01.Person");

//创建运行时类的对象
Person person = clazz.newInstance();

//1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
Field name = clazz.getDeclaredField("name");

//2.保证当前属性是可访问的
name.setAccessible(true);

//3.获取、设置指定对象的此属性值
name.set(person, "Jermyn");
System.out.println(name.get(person));
}

// 调方法
@Test
public void test03() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {

Class<Person> clazz = (Class<Person>) Class.forName("cn.jermyn.test01.Person");

//创建运行时类的对象
Person person = clazz.newInstance();

//1.获取指定的某个方法 getDeclaredMethod():参数1 :指明获取的方法的名称 参数2:指明获取的方法的形参列表
Method show = clazz.getDeclaredMethod("showNation", String.class);

//2.保证当前方法是可访问的
show.setAccessible(true);

//3. 调用方法的invoke(参数1,参数2):参数1:方法的调用者 参数2:给方法形参赋值的实参invoke()的返回值即为对应类中调用的方法的返回值。
Object o = show.invoke(person, "CHN");
String str = (String) o;
System.out.println(o);
}

// 调静态方法
@Test
public void test4() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {

Class<Person> clazz = (Class<Person>) Class.forName("cn.jermyn.test01.Person");

Method showDesc = clazz.getDeclaredMethod("showDesc");

showDesc.setAccessible(true);

//如果调用的运行时类中的方法没有返回值,则此invoke()返回null
Object returnVal = showDesc.invoke(Person.class);
System.out.println(returnVal);
}

// 调构造器
@Test
public void test05() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<Person> clazz = (Class<Person>) Class.forName("cn.jermyn.test01.Person");

// 1.获取指定的构造器getDeclaredConstructor():参数:指明构造器的参数列表
Constructor constructor = clazz.getDeclaredConstructor(String.class);

//2.保证此构造器是可访问的
constructor.setAccessible(true);

//3.调用此构造器创建运行时类的对象
Person per = (Person) constructor.newInstance("Jermyn");
System.out.println(per);
}
}

点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
D:\PATH-EN\java-path\bin\java.exe ...
Person()
1001
Person()
Jermyn
Person()
我的国籍是:CHN
CHN
Person{name='Jermyn', age=0}
I am superman!
null

===============================================
Default Suite
Total tests run: 5, Passes: 5, Failures: 0, Skips: 0
===============================================


Process finished with exit code 0

反射的应用:动态代理

静态代理

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
interface ClothFactory {

void produceCloth();

}

// 代理类
class ProxyClothFactory implements ClothFactory {

private ClothFactory factory;

public ProxyClothFactory(ClothFactory factory) {
this.factory = factory;
}

@Override
public void produceCloth() {
System.out.println("代理工厂准备工作");

factory.produceCloth();

System.out.println("代理工厂做收尾工作");
}
}

// 被代理类
class AntaClothFactory implements ClothFactory {

@Override
public void produceCloth() {
System.out.println("安踏工厂生产产品");
}
}

public class StaticProxyTest {

public static void main(String[] args) {

AntaClothFactory anta = new AntaClothFactory();

ProxyClothFactory pcf = new ProxyClothFactory(anta);

pcf.produceCloth();
}
}

点击查看运行结果
1
2
3
4
5
6
D:\PATH-EN\java-path\bin\java.exe ...
代理工厂准备工作
安踏工厂生产产品
代理工厂做收尾工作

进程已结束,退出代码0
  • 代理设计模式的原理:使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原 始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原 始对象上。
  • 动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时 根据需要动态创建目标类的代理对象。
  • 动态代理使用场合:

    • 调试
    • 远程方法调用
  • 动态代理相比于静态代理的优点:抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中 处理,这样,我们可以更加灵活和统一的处理众多的方法。

  • Java动态代理相关API

    • Proxy :专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。
    • 提供用于创建动态代理类和动态代理对象的静态方法
      • static Class<?>getProxyClass(ClassLoader loader, Class<?>… interfaces) 创建一个动态代理类所对应的Class对象
      • static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) 直接创建一个动态代理对象
  • 动态代理步骤

  1. 创建一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。
  2. 创建被代理的类以及接口
  3. 通过Proxy的静态方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个Subject接口代理
  4. 通过 Subject代理调用RealSubject实现类的方法

    动态代理

    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;

    interface Human {

    String getBelief();

    void eat(String food);
    }

    //2. 创建被代理的类以及接口
    class SuperMan implements Human {

    @Override
    public String getBelief() {
    return "Peace and love!";
    }

    @Override
    public void eat(String food) {
    System.out.println("我喜欢吃" + food);
    }
    }


    class ProxyFactory {

    public static Object getProxyInstance(Object obj) {

    MyInvocationHandler handler = new MyInvocationHandler();

    handler.bind(obj);
    // 3.通过Proxy的静态方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个Subject接口代理
    return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
    }
    }

    //1. 创建一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。
    class MyInvocationHandler implements InvocationHandler {

    //obj:被代理类的对象
    private Object obj;

    public void bind(Object obj) {
    this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
    //obj:被代理类的对象
    Object invoke = method.invoke(obj, args);

    //上述方法的返回值就作为当前类中的invoke()的返回值。
    return invoke;
    }
    }

    public class ProxyTest {

    public static void main(String[] args) {

    SuperMan superMan = new SuperMan();
    //4. 通过ProxyFactory代理调用getProxyInstance实现类的方法
    Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);

    String belief = proxyInstance.getBelief();
    System.out.println(belief);

    proxyInstance.eat("KFC");
    }
    }

    点击查看运行结果
    1
    2
    3
    4
    5
    D:\PATH-EN\java-path\bin\java.exe ...
    Peace and love!
    我喜欢吃KFC

    进程已结束,退出代码0

第九章 Java8的其它新特性

Lambda表达式

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以 传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更 灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

  • Lambda 表达式:
    在Java 8 语言中引入的一种新的语法元素和操 作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符 或箭头操作符。它将 Lambda 分为两个部分:
    左侧:指定了 Lambda 表达式需要的参数列表
    右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即Lambda 表达式要执行的功能。

  • 格式
    语法格式一:无参,无返回值
    语法格式二:Lambda 需要一个参数,但是没有返回值。
    语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
    语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
    语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
    语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略

    示例

    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    import org.junit.Test;

    import java.util.Comparator;
    import java.util.function.Consumer;

    /**
    * Description:
    * -> :lambda操作符 或 箭头操作符
    * ->左边:lambda形参列表 (其实就是接口中的抽象方法的形参列表)
    * ->右边:lambda体 (其实就是重写的抽象方法的方法体)
    * Author: Jermyn
    */
    public class LambdaTest {

    // 语法格式一:无参,无返回值
    @Test
    public void test01() {
    Runnable r1 = new Runnable() {
    @Override
    public void run() {
    System.out.println("无参数,无返回值");
    }
    };
    r1.run();

    // 左边为形参列表,无则为(),右边方法体
    Runnable r2 = () -> {
    System.out.println("无参数,无返回值");
    };
    r2.run();
    }

    //语法格式二:Lambda 需要一个参数,但是没有返回值。
    @Test
    public void test02() {

    Consumer<String> consumer = new Consumer<String>() {
    @Override
    public void accept(String s) {
    System.out.println(s);
    }
    };

    consumer.accept("To be,or not to be, that is the Question");

    Consumer<String> con = (String s) -> {
    System.out.println(s);
    };
    con.accept("生存即毁灭,死亡即永恒");
    }

    // 语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
    @Test
    public void test03() {
    // 类型推断
    Consumer<String> con = (str) -> {
    System.out.println(str);
    };
    con.accept("生存即毁灭,死亡即永恒");
    }

    // 语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
    @Test
    public void test04() {
    // 省去str的小括号
    Consumer<String> con = str -> {
    System.out.println(str);
    };
    con.accept("生存即毁灭,死亡即永恒");
    }

    // 语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
    @Test
    public void test05() {
    Comparator<Integer> comparator = (x, y) -> {
    return x.compareTo(y);
    };

    System.out.println(comparator.compare(12, 21));
    }

    // 语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
    @Test
    public void test06() {

    Comparator<Integer> comparator = (x, y) -> x.compareTo(y);
    System.out.println(comparator.compare(21, 12));
    }
    }

    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    D:\PATH-EN\java-path\bin\java.exe ...
    无参数,无返回值
    无参数,无返回值
    To be,or not to be, that is the Question
    生存即毁灭,死亡即永恒
    生存即毁灭,死亡即永恒
    生存即毁灭,死亡即永恒
    -1
    1

    Process finished with exit code 0

    总结:
    ->左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只有一个参数,其一对()也可以省略
    ->右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一对{}和return关键字

  • 类型推断
    上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序 的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于 上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。

函数式(Functional)接口

  • 什么是函数式(Functional)接口

    • 只包含一个抽象方法的接口,称为函数式接口。
    • 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式 抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽 象方法上进行声明)。
    • 我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检 查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个 接口是一个函数式接口。
    • 在java.util.function包下定义了Java 8 的丰富的函数式接口
  • 如何理解函数式接口

    • Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP) 编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不 得不做出调整以便支持更加广泛的技术要求,也即java不但可以支持OOP还 可以支持OOF(面向函数编程)
    • 在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的 编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在 Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的 对象类型——函数式接口。
    • 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是 Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口 的实例,那么该对象就可以用Lambda表达式来表示。
    • 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。
      点击图片可以跳转源码
  • Java 内置四大核心函数式接口

  • 其他接口

方法引用与构造器引用

  • 方法引用(Method References)

    • 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
    • 方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就 是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向 一个方法,可以认为是Lambda表达式的一个语法糖。
    • 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的 方法的参数列表和返回值类型保持一致!
    • 格式:使用操作符 “::” 将类(或对象)与方法名分隔开来。
    • 如下三种主要使用情况:
      • 对象::实例方法名
      • 类::静态方法名
      • 类::实例方法名

        示例

        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
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        public class MethodRefTest {

        // 情况一:对象 :: 实例方法
        //Consumer中的void accept(T t)
        //PrintStream中的void println(T t)
        @Test
        public void test1() {
        Consumer<String> con1 = str -> System.out.println(str);
        con1.accept("北京");

        System.out.println("*******************");
        PrintStream ps = System.out;
        Consumer<String> con2 = ps::println;
        con2.accept("beijing");
        }

        //Supplier中的T get()
        //Employee中的String getName()
        @Test
        public void test2() {
        Employee emp = new Employee(1001,"Tom",23,5600);

        Supplier<String> sup1 = () -> emp.getName();
        System.out.println(sup1.get());

        System.out.println("*******************");
        Supplier<String> sup2 = emp::getName;
        System.out.println(sup2.get());

        }

        // 情况二:类 :: 静态方法
        //Comparator中的int compare(T t1,T t2)
        //Integer中的int compare(T t1,T t2)
        @Test
        public void test3() {
        Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
        System.out.println(com1.compare(12,21));

        System.out.println("*******************");

        Comparator<Integer> com2 = Integer::compare;
        System.out.println(com2.compare(12,3));

        }

        //Function中的R apply(T t)
        //Math中的Long round(Double d)
        @Test
        public void test4() {
        Function<Double,Long> func = new Function<Double, Long>() {
        @Override
        public Long apply(Double d) {
        return Math.round(d);
        }
        };

        System.out.println("*******************");

        Function<Double,Long> func1 = d -> Math.round(d);
        System.out.println(func1.apply(12.3));

        System.out.println("*******************");

        Function<Double,Long> func2 = Math::round;
        System.out.println(func2.apply(12.6));
        }

        // 情况三:类 :: 实例方法 (有难度)
        // Comparator中的int comapre(T t1,T t2)
        // String中的int t1.compareTo(t2)
        @Test
        public void test5() {
        Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
        System.out.println(com1.compare("abc","abd"));

        System.out.println("*******************");

        Comparator<String> com2 = String :: compareTo;
        System.out.println(com2.compare("abd","abm"));
        }

        //BiPredicate中的boolean test(T t1, T t2);
        //String中的boolean t1.equals(t2)
        @Test
        public void test6() {
        BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
        System.out.println(pre1.test("abc","abc"));

        System.out.println("*******************");
        BiPredicate<String,String> pre2 = String :: equals;
        System.out.println(pre2.test("abc","abd"));
        }

        // Function中的R apply(T t)
        // Employee中的String getName();
        @Test
        public void test7() {
        Employee employee = new Employee(1001, "Jerry", 23, 6000);


        Function<Employee,String> func1 = e -> e.getName();
        System.out.println(func1.apply(employee));

        System.out.println("*******************");


        Function<Employee,String> func2 = Employee::getName;
        System.out.println(func2.apply(employee));
        }
        }

  • 构造器引用
    格式:ClassName::new

    • 与函数式接口相结合,自动与函数式接口中方法兼容。
    • 可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象
    • 方法的参数列表一致!且方法的返回值即为构造器对应类的对象。
  • 数组引用
    格式: type[] :: new

    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
    61
    62
    63
    64
    65
    66
    67
    68
    public class ConstructorRefTest {
    //构造器引用
    //Supplier中的T get()
    //Employee的空参构造器:Employee()
    @Test
    public void test1(){

    Supplier<Employee> sup = new Supplier<Employee>() {
    @Override
    public Employee get() {
    return new Employee();
    }
    };
    System.out.println("*******************");

    Supplier<Employee> sup1 = () -> new Employee();
    System.out.println(sup1.get());

    System.out.println("*******************");

    Supplier<Employee> sup2 = Employee :: new;
    System.out.println(sup2.get());
    }

    //Function中的R apply(T t)
    @Test
    public void test2(){
    Function<Integer,Employee> func1 = id -> new Employee(id);
    Employee employee = func1.apply(1001);
    System.out.println(employee);

    System.out.println("*******************");

    Function<Integer,Employee> func2 = Employee :: new;
    Employee employee1 = func2.apply(1002);
    System.out.println(employee1);

    }

    //BiFunction中的R apply(T t,U u)
    @Test
    public void test3(){
    BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
    System.out.println(func1.apply(1001,"Tom"));

    System.out.println("*******************");

    BiFunction<Integer,String,Employee> func2 = Employee :: new;
    System.out.println(func2.apply(1002,"Tom"));

    }

    //数组引用
    //Function中的R apply(T t)
    @Test
    public void test4(){
    Function<Integer,String[]> func1 = length -> new String[length];
    String[] arr1 = func1.apply(5);
    System.out.println(Arrays.toString(arr1));

    System.out.println("*******************");

    Function<Integer,String[]> func2 = String[] :: new;
    String[] arr2 = func2.apply(10);
    System.out.println(Arrays.toString(arr2));

    }
    }

强大的Stream API

  • Stream API说明

    • Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则 是 Stream API。
    • Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这 是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程 序员的生产力,让程序员写出高效率、干净、简洁的代码。
    • Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进 行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。 也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种 高效且易于使用的处理数据的方式。
  • 注意:

    1. Stream 自己不会存储元素。
    2. Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
    3. Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
  • Stream 的操作三个步骤

    1. 创建 Stream:一个数据源(如:集合、数组),获取一个流
    2. 中间操作:一个中间操作链,对数据源的数据进行处理
    3. 终止操作(终端操作):一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
  • 创建 Stream
    方式一:通过集合Java8 中的 Collection 接口被扩展,提供了两个获取流 的方法:

    • default Streamstream() : 返回一个顺序流
    • default StreamparallelStream() : 返回一个并行流

方式二:通过数组Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
staticStreamstream(T[] array): 返回一个流 重载形式,能够处理对应基本类型的数组:

  • public static IntStream stream(int[] array)
  • public static LongStream stream(long[] array)
  • public static DoubleStream stream(double[] array)

方式三:通过Stream的of()可以调用Stream类静态方法 of(), 通过显示值创建一个 流。它可以接收任意数量的参数。

  • public staticStreamof(T… values) : 返回一个流

方式四:创建无限流可以使用静态方法 Stream.iterate() 和 Stream.generate(),
创建无限流。

  1. 迭代public staticStreamiterate(final T seed, final UnaryOperatorf)
  2. 生成public staticStreamgenerate(Suppliers)

    示例

    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
    import org.junit.Test;

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.IntStream;
    import java.util.stream.Stream;

    public class StreamAPITest {

    //创建 Stream方式一:通过集合
    @Test
    public void test01() {
    List<Employee> employees = EmployeeData.getEmployees();

    // default Stream<E> stream() : 返回一个顺序流
    Stream<Employee> stream = employees.stream();

    // default Stream<E> parallelStream() : 返回一个并行流
    Stream<Employee> employeeStream = employees.parallelStream();
    }

    //创建 Stream方式二:通过数组
    @Test
    public void test02() {
    int[] arr = new int[]{8, 2, 7, 3, 8};
    IntStream stream = Arrays.stream(arr);

    Employee e1 = new Employee(12, "Jermyn", 6000);
    Employee e2 = new Employee(113, "Jack", 3000);
    Employee[] employees = new Employee[]{e1, e2};
    Stream<Employee> stream1 = Arrays.stream(employees);
    }

    //创建 Stream方式三:通过Stream的of()
    @Test
    public void test03() {
    Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
    }

    //创建 Stream方式四:创建无限流
    @Test
    public void test04() {

    // 迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
    //遍历前10个偶数
    Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);

    // 生成:public static<T> Stream<T> generate(Supplier<T> s)
    Stream.generate(Math::random).limit(10).forEach(System.out::println);
    }
    }

  • Stream 的中间操作
    多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。
  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
    @Test
    public void test01() {

    List<Employee> employees = EmployeeData.getEmployees();

    // filter(Predicate p)——接收 Lambda ,从流中排除某些元素。
    Stream<Employee> stream = employees.stream();
    stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println);

    System.out.println();

    // 第一个Stream已经终止
    // limit(n)——截断流,使其元素不超过给定数量。
    Stream<Employee> stream2 = employees.stream();
    stream2.limit(5).forEach(System.out::println);

    System.out.println();

    // skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
    Stream<Employee> stream3 = employees.stream();
    stream3.skip(1).forEach(System.out::println);

    System.out.println();

    // distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
    employees.add(new Employee(1010,"刘强东",40,8000));
    employees.add(new Employee(1010,"刘强东",40,8000));
    employees.add(new Employee(1010,"刘强东",40,8000));

    // System.out.println(employees);
    employees.stream().distinct().forEach(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
    D:\PATH-EN\java-path\bin\java.exe ...
    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}

    Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1003, name='刘强东', age=33, salary=3000.82}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1005, name='李彦宏', age=65, salary=5555.32}

    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1003, name='刘强东', age=33, salary=3000.82}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
    Employee{id=1007, name='任正非', age=26, salary=4333.32}
    Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}

    Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1003, name='刘强东', age=33, salary=3000.82}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
    Employee{id=1007, name='任正非', age=26, salary=4333.32}
    Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
    Employee{id=1010, name='刘强东', age=40, salary=8000.0}

    Process finished with exit code 0
  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
    @Test
    public void test02() {

    // map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。
    List<Employee> employees = EmployeeData.getEmployees();

    Stream<Employee> stream = employees.stream();
    Stream<String> stringStream = stream.map(e -> e.getName()).filter(name -> name.length() > 3);
    stringStream.forEach(System.out::println);

    System.out.println();

    List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
    Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest::fromStringToStream);
    streamStream.forEach(e -> {
    e.forEach(System.out::println);
    });

    System.out.println();

    // flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
    Stream<Character> characterStream = list.stream().flatMap(StreamAPITest::fromStringToStream);
    characterStream.forEach(System.out::println);
    }

    //将字符串中的多个字符构成的集合转换为对应的Stream的实例
    public static Stream<Character> fromStringToStream(String str) {//aa
    ArrayList<Character> list = new ArrayList<>();
    for (Character c : str.toCharArray()) {
    list.add(c);
    }
    return list.stream();
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    D:\PATH-EN\java-path\bin\java.exe ...
    比尔盖茨
    扎克伯格

    a
    a
    b
    b
    c
    c
    d
    d

    a
    a
    b
    b
    c
    c
    d
    d

    Process finished with exit code 0
  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
    @Test
    public void test04() {

    // 自然排序
    List<Integer> integers = Arrays.asList(11, 12, 13, 23, 2, 3, 1);
    integers.stream().sorted().forEach(System.out::println);

    System.out.println();

    // 定制排序
    // 按照年龄从大到小排序
    List<Employee> employees = EmployeeData.getEmployees();
    Stream<Employee> stream = employees.stream();
    stream.sorted((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())).forEach(System.out::println);

    System.out.println();

    // 当年龄一样是按照薪水从大到小
    Stream<Employee> stream1 = employees.stream();
    stream1.sorted((e1, e2) -> {
    int ageValue = Integer.compare(e1.getAge(), e2.getAge());
    if (ageValue != 0) {
    return ageValue;
    } else {
    return Double.compare(e1.getSalary(), e2.getSalary());
    }
    }).forEach(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
    D:\PATH-EN\java-path\bin\java.exe ...
    1
    2
    3
    11
    12
    13
    23

    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1007, name='任正非', age=26, salary=4333.32}
    Employee{id=1003, name='刘强东', age=33, salary=3000.82}
    Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
    Employee{id=1005, name='李彦宏', age=65, salary=5555.32}

    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1007, name='任正非', age=26, salary=4333.32}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1003, name='刘强东', age=33, salary=3000.82}
    Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
    Employee{id=1005, name='李彦宏', age=65, salary=5555.32}

    Process finished with exit code 0
  • 终止操作
    • 终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。
    • 流进行了终止操作后,不能再次使用。
  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
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    // 匹配与查找
    @Test
    public void test01() {

    List<Employee> employees = EmployeeData.getEmployees();
    Stream<Employee> stream = employees.stream();

    // allMatch(Predicate p)——检查是否匹配所有元素。
    // 练习:是否所有的员工的年龄都大于18
    boolean b = stream.allMatch(employee -> employee.getAge() > 18);
    System.out.println(b);

    System.out.println();

    // anyMatch(Predicate p)——检查是否至少匹配一个元素。
    // 练习:是否存在员工的工资大于 10000
    Stream<Employee> stream1 = employees.stream();
    boolean b1 = stream1.anyMatch(employee -> employee.getSalary() > 10000);
    System.out.println(b1);

    System.out.println();

    // noneMatch(Predicate p)——检查是否没有匹配的元素。
    // 练习:是否**存在**员工姓“雷”
    Stream<Employee> stream2 = employees.stream();
    boolean lei = stream2.noneMatch(employee -> employee.getName().startsWith("雷"));
    System.out.println(lei);

    System.out.println();

    // findFirst——返回第一个元素
    Stream<Employee> stream3 = employees.stream();
    Optional<Employee> first = stream3.findFirst();
    System.out.println(first);


    System.out.println();

    // findAny——返回当前流中的任意元素
    Stream<Employee> stream4 = employees.parallelStream();
    Optional<Employee> any = stream4.findAny();
    System.out.println(any);
    }

    @Test
    public void test02() {

    List<Employee> employees = EmployeeData.getEmployees();

    // count——返回流中元素的总个数
    Stream<Employee> stream = employees.stream();
    long count = stream.count();
    System.out.println(count);

    System.out.println();

    // max(Comparator c)——返回流中最大值
    // 练习:返回最高的工资:
    Stream<Employee> stream1 = employees.stream();
    Stream<Double> doubleStream = stream1.map(employee -> employee.getSalary());
    Optional<Double> max = doubleStream.max((e1, e2) -> Double.compare(e1, e2));
    System.out.println(max);

    System.out.println();

    // min(Comparator c)——返回流中最小值
    // 练习:返回最低工资的员工
    Stream<Employee> stream2 = employees.stream();
    Optional<Employee> min = stream2.min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
    System.out.println(min);

    System.out.println();

    // forEach(Consumer c)——内部迭代
    employees.stream().forEach(System.out::println);

    System.out.println();

    //使用集合的遍历操作
    employees.forEach(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
    D:\PATH-EN\java-path\bin\java.exe ...
    false

    false

    false

    Optional[Employee{id=1001, name='马化腾', age=34, salary=6000.38}]

    Optional[Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}]
    8

    Optional[9876.12]

    Optional[Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}]

    Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1003, name='刘强东', age=33, salary=3000.82}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
    Employee{id=1007, name='任正非', age=26, salary=4333.32}
    Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}

    Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1003, name='刘强东', age=33, salary=3000.82}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
    Employee{id=1007, name='任正非', age=26, salary=4333.32}
    Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}

    Process finished with exit code 0
  2. 归约
    map 和 reduce 的连接通常称为 map-reduce 模式,因 Google用它来进行网络搜索而出名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Test
    public void test03() {

    List<Integer> integers = Arrays.asList(1, 2, 3, 5, 6, 4);

    // reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T
    // 练习1:计算1-10的自然数的和
    Stream<Integer> stream = integers.stream();
    Integer reduce = stream.reduce(0, Integer::sum);
    System.out.println(reduce);

    System.out.println();

    // reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
    // 练习2:计算公司所有员工工资的总和
    List<Employee> employees = EmployeeData.getEmployees();

    Optional<Double> reduce1 = employees.stream().map(employee -> employee.getSalary()).reduce(Double::sum);
    System.out.println(reduce1);
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    D:\PATH-EN\java-path\bin\java.exe ...
    21

    Optional[48424.08]

    Process finished with exit code 0
  3. 收集
    Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、 Map)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    public void test04() {

    // collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
    // 练习1:查找工资大于6000的员工,结果返回为一个List或Set
    List<Employee> employees = EmployeeData.getEmployees();
    Stream<Employee> stream = employees.stream();
    Stream<Employee> employeeStream = stream.filter(employee -> employee.getSalary() > 6000);
    List<Employee> collect = employeeStream.collect(Collectors.toList());
    collect.forEach(System.out::println);
    }
    点击查看运行结果
    1
    2
    3
    4
    5
    6
    7
    D:\PATH-EN\java-path\bin\java.exe ...
    Employee{id=1001, name='马化腾', age=34, salary=6000.38}
    Employee{id=1002, name='马云', age=12, salary=9876.12}
    Employee{id=1004, name='雷军', age=26, salary=7657.37}
    Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}

    Process finished with exit code 0

Optional类

  • Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
  • 创建Optional类对象的方法:

    • Optional.of(T t) : 创建一个 Optional 实例,t必须非空;
    • Optional.empty() : 创建一个空的 Optional 实例
    • Optional.ofNullable(T t):t可以为null
  • 判断Optional容器中是否包含对象:

    • boolean isPresent() : 判断是否包含对象
    • void ifPresent(Consumer<? super T> consumer) :如果有值,就执行Consumer 接口的实现代码,并且该值会作为参数传给它。
  • 获取Optional容器的对象:

    • T get(): 如果调用对象包含值,返回该值,否则抛异常
    • T orElse(T other) :如果有值则将其返回,否则返回指定的other对象。
    • T orElseGet(Supplier<? extends T> other) :如果有值则将其返回,否则返回由Supplier接口实现提供的对象。
    • T orElseThrow(Supplier<? extends X> exceptionSupplier) :如果有值则将其返 回,否则抛出由Supplier接口实现提供的异常。

第十章 Java9&10&11新特性

Java 9 的新特性

  • JDK 和 JRE 目录结构的改变

    • JDK 8 的目录结构
    • JDK 9 的目录结构
  • 模块化系统: Jigsaw -> Modularity
    本质上讲也就是说,用模块来管理各个package,通过声明某个package 暴露,,模块(module)的概念,其实就是package外再裹一层,不声明默 认就是隐藏。因此,模块化使得代码组织上更安全,因为它可以指定哪 些部分可以暴露,哪些部分隐藏。

    • 实现目标
      • 模块化的主要目的在于减少内存的开销
      • 只须必要模块,而非全部jdk模块,可简化各种类库和大型应用的开 发和维护
      • 改进 Java SE 平台,使其可以适应不同大小的计算设备
      • 改进其安全性,可维护性,提高性能

模块将由通常的类和新的模块声明文件(module-info.java)组成。该文件是位于 java代码结构的顶层,该模块描述符明确地定义了我们的模块需要什么依赖关系,以及哪些模块被外部使用。在exports子句中未提及的所有包默认情况下将封装在模块中,不能在外部使用。

  • Java的REPL工具: jShell命令

    • 实现目标
      • Java 9 中终于拥有了 REPL工具:jShell。让Java可以像脚本语言一样运行,从 控制台启动jShell,利用jShell在没有创建类的情况下直接声明变量,计算表达式, 执行语句。即开发时可以在命令行里直接运行Java的代码,而无需创建Java文
        件,无需跟人解释”public static void main(String[] args)”这句废话。
      • jShell也可以从文件中加载语句或者将语句保存到文件中。
      • jShell也可以是tab键进行自动补全和自动添加分号。
  • 语法改进:接口的私有方法
    Java 8中规定接口中的方法除了抽象方法之外,还可以定义静态方法 和默认的方法。一定程度上,扩展了接口的功能,此时的接口更像是 一个抽象类。

在Java9中,接口更加的灵活和强大,连方法的访问权限修饰符都可 以声明为private的了,此时方法将不会成为你对外暴露的API的一部分。

  • 语法改进:钻石操作符使用升级
    我们将能够与匿名实现类共同使用钻石操作符(diamond operator)在Java 8中如下的操作是会报错的:

    1
    2
    3
    4
    5
    6
    Comparator<Object> com = new Comparator<>() {
    @Override
    public int compare(Object o1, Object o2) {
    return 0;
    }
    };

    Java 9中如下操作可以正常执行通过:

    1
    2
    3
    4
    5
    6
    7
    // anonymous classes can now use type inference
    Comparator<Object> com = new Comparator<>(){
    @Override
    public int compare(Object o1, Object o2) {
    return 0;
    }
    };
  • 语法改进:try语句
    Java 8 中,可以实现资源的自动关闭,但是要求执行后必须关闭的所有资源必须在try子句中初始化,否则编译不通过。如下例所示:

    1
    2
    3
    4
    5
    try(InputStreamReader reader = new InputStreamReader(System.in)) {
    //读取数据细节省略
    } catch (IOException e) {
    e.printStackTrace();
    }

    Java 9中,用资源语句编写try将更容易,我们可以在try子句中使用已经初始 化过的资源,此时的资源是final的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    InputStreamReader reader = new InputStreamReader(System.in);  
    OutputStreamWriter writer = new OutputStreamWriter(System.out); \
    try (reader; writer) {
    //reader是final的,不可再被赋值
    //reader = null;
    //具体读写操作省略
    } catch (IOException e) {
    e.printStackTrace();
    }
  • String存储结构变更
    Motivation
    The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.
    Description
    We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.
    结论:String 再也不用 char[] 来存储,改成了 byte[] 加上编码标记,节约了一些空间。

    1
    2
    3
    4
    public final class String implements java.io.Serializable, Comparable<String>, CharSequence {  
    @Stable
    private final
    }
  • 集合工厂方法:快速创建只读集合
    要创建一个只读、不可改变的集合,必须构造和分配它,然后添加元素,最后 包装成一个不可修改的集合。

    1
    2
    3
    4
    5
    6
    7
    List<String> namesList = new ArrayList <>();  
    namesList.add("Joe");
    namesList.add("Bob");
    namesList.add("Bill");

    namesList = Collections.unmodifiableList(namesList);
    System.out.println(namesList);

    Java 9因此引入了方便的方法,这使得类似的事情更容易表达。
    点击图片可以跳转源码

  • InputStream 加强
    InputStream 终于有了一个非常有用的方法:transferTo,可以用来将数据直接 传输到 OutputStream,这是在处理原始数据流时非常常见的一种用法,如下示例。

    1
    2
    3
    4
    5
    6
    7
    ClassLoader cl = this.getClass().getClassLoader();
    try (InputStream is = cl.getResourceAsStream("hello.txt");
    OutputStream os = new FileOutputStream("src\\hello1.txt")) {
    is.transferTo(os); // 把输入流中的所有数据直接自动地复制到输出流中
    } catch (IOException e) {
    e.printStackTrace();
    }
  • 增强的 Stream API

    • Java 的 Steam API 是java标准库最好的改进之一,让开发者能够快速运算, 从而能够有效的利用数据并行计算。Java 8 提供的 Steam 能够利用多核架构 实现声明式的数据处理。
    • 在Java 9 中,Stream API 变得更好,Stream 接口中添加了 4 个新的方法: takeWhile, dropWhile, ofNullable,还有个 iterate 方法的新重载方法,可以 让你提供一个Predicate (判断条件)来指定什么时候结束迭代。
    • 除了对 Stream 本身的扩展,Optional 和 Stream 之间的结合也得到了改进。 现在可以通过 Optional 的新方法 stream() 将一个 Optional 对象转换为一个 (可能是空的) Stream 对象。
  1. takeWhile()的使用
    用于从 Stream 中获取一部分数据,接收一个 Predicate 来进行选择。在有序的Stream 中,takeWhile 返回从开头开始的尽量多的元素。
    1
    2
    3
    4
    5
    6
    7
    List<Integer> list = Arrays.asList(45, 43, 76, 87, 42, 77, 90, 73, 67, 88);  
    list.stream().takeWhile(x -> x < 50).forEach(System.out::println);

    System.out.println();

    list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
    list.stream().takeWhile(x -> x < 5).forEach(System.out::println);
  2. dropWhile()的使用
    dropWhile 的行为与takeWhile 相反,返回剩余的元素。
    1
    2
    3
    4
    5
    List<Integer> list = Arrays.asList(45, 43, 76, 87, 42, 77, 90, 73, 67, 88);  
    list.stream().dropWhile(x -> x < 50).forEach(System.out::println);
    System.out.println();
    list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
    list.stream().dropWhile(x -> x < 5).forEach(System.out::println);
  3. ofNullable()的使用
    Java 8 中 Stream 不能完全为null,否则会报空指针异常。而 Java 9 中的ofNullable方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可以创建一个空 Stream。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 报 NullPointerException
    // Stream<Object> stream1 = Stream.of(null);
    // System.out.println(stream1.count());
    // 不报异常,允许通过
    Stream<String> stringStream = Stream.of("AA", "BB", null);
    System.out.println(stringStream.count());// 3
    // 不报异常,允许通过
    List<String> list = new ArrayList<>();
    list.add("AA");
    list.add(null);
    System.out.println(list.stream().count());// 2
    // ofNullable():允许值为null
    Stream<Object> stream1 = Stream.ofNullable(null);
    System.out.println(stream1.count());// 0
    Stream<String> stream = Stream.ofNullable("hello world");
    System.out.println(stream.count());// 1
  4. iterate()重载的使用
    这个 iterate 方法的新重载方法,可以让你提供一个 Predicate (判断条件)来指定什么时候结束迭代。
    1
    2
    3
    4
    // 原来的控制终止方式:
    Stream.iterate(1, i -> i + 1).limit(10).forEach(System.out::println);
    // 现在的终止方式:
    Stream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println);
  • Optional获取Stream的方法
    Optional类中stream()的使用

    1
    2
    3
    4
    5
    6
    7
    8
    List<String> list = new ArrayList<>();
    list.add("Tom");
    list.add("Jerry");
    list.add("Tim");

    Optional<List<String>> optional = Optional.ofNullable(list);
    Stream<List<String>> stream = optional.stream();
    stream.flatMap(x -> x.stream()).forEach(System.out::println);
  • Javascript引擎升级:Nashorn

    • Nashorn 项目在 JDK 9 中得到改进,它为 Java 提供轻量级的 Javascript 运行时。 Nashorn 项目跟随 Netscape 的 Rhino 项目,目的是为了在 Java 中实现一个高 性能但轻量级的 Javascript 运行时。Nashorn 项目使得 Java 应用能够嵌入 Javascript。它在 JDK 8 中为Java 提供一个Javascript 引擎。
    • JDK 9 包含一个用来解析 Nashorn 的 ECMAScript 语法树的 API。这个 API 使得 IDE 和服务端框架不需要依赖 Nashorn 项目的内部实现类, 就能够分析 ECMAScript 代码。

Java 10 新特性

  • 局部变量类型推断
    好处:减少了啰嗦和形式的代码,避免了信息冗余,而且对齐了变量名,更容易阅读!
    场景一:类实例化时
    作为 Java开发者,在声明一个变量时,我们总是习惯了敲打两次变量类型,第一次用于声明变量类型,第二次用于构造器。

    1
    LinkedHashSet<Integer> set = new LinkedHashSet<>();

    场景二:返回值类型含复杂泛型结构
    变量的声明类型书写复杂且较长,尤其是加上泛型的使用

    1
    Iterator<Map.Entry<Integer, Student>> iterator = set.iterator();

    场景三:我们也经常声明一种变量,它只会被使用一次,而且是用在下一行代码中,比如:

    1
    2
    3
    URL url = new URL("https://jermyn.cn/");  
    URLConnection connection = url.openConnection();
    Reader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));

    尽管IDE可以帮我们自动完成这些代码,但当变量总是跳来跳去的时候,可读 性还是会受到影响,因为变量类型的名称由各种不同长度的字符组成。而且, 有时候开发人员会尽力避免声明中间变量,因为太多的类型声明只会分散注意 力,不会带来额外的好处。

  • 局部变量类型推断适用于以下情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //1.局部变量的初始化
    var list = new ArrayList<>();

    //2.增强for循环中的索引
    for(var v : list) {
    System.out.println(v);
    }

    //3.传统for循环中
    for(var i = 0;i < 100;i++) {
    System.out.println(i);
    }
  • 局部变量类型推断如下情况不适用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Test
    public void test() {
    //1.局部变量不赋值,就不能实现类型推断
    var num ;

    //2.lambda表示式中,左边的函数式接口不能声明为var
    Supplier<Double> sup = () -> Math.random();
    var sup = () -> Math.random();

    //3.方法引用中,左边的函数式接口不能声明为var
    Consumer<String> con = System.out::println;
    var con = System.out::println;

    //4.数组的静态初始化中,注意如下的情况也不可以
    int[] arr = {1, 2, 3, 4};
    var arr = {1,2,3,4};
    }

    不适用以下的结构中:
    情况1:没有初始化的局部变量声明
    情况2:方法的返回类型
    情况3:方法的参数类型
    情况4:构造器的参数类型
    情况5:属性
    情况6:catch块

    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
    @Test
    public void test() {
    // 情况1:没有初始化的局部变量声明
    var s = null;

    // 情况6:catch块
    try{

    }catch(var e){
    e.printStackTrace();
    }

    //情况2:方法的返回类型
    public var method1(){

    return 0;
    }

    // 情况3:方法的参数类型
    public void method2(var num){

    }

    //情况4:构造器的参数类型
    public Java10Test(var i){

    }

    //情况5:属性
    var num;
    }
  • 工作原理
    在处理 var时,编译器先是查看表达式右边部分,并根据右边变量值的类型进行 推断,作为左边变量的类型,然后将该类型写入字节码当中。

  • 注意
    var不是一个关键字
    你不需要担心变量名或方法名会与 var发生冲突,因为 var实际上并不是一个关键字,
    而是一个类型名,只有在编译器需要知道类型的地方才需要用到它。除此之外,它 就是一个普通合法的标识符。也就是说,除了不能用它作为类名,其他的都可以, 但极少人会用它作为类名。
    这不是JavaScript
    首先我要说明的是,var并不会改变Java是一门静态类型语言的事实。编译器负责推 断出类型,并把结果写入字节码文件,就好像是开发人员自己敲入类型一样。

  • 集合新增创建不可变集合的方法
    自 Java 9 开始,Jdk 里面为集合(List / Set / Map)都添加了 of (jdk9新增)和 copyOf (jdk10新增)方法,它们两个都用来创建不可变的集合,来看下它们的 使用和区别。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //示例1:
    var list1 = List.of("Java", "Python", "C");
    var copy1 = List.copyOf(list1);
    System.out.println(list1 == copy1); // true

    //示例2:
    var list2 = new ArrayList<String>();
    var copy2 = List.copyOf(list2);
    System.out.println(list2 == copy2); // false
    //示例1和2代码基本一致,为什么一个为true,一个为false?

    从源码分析 ,可以看出 copyOf方法会先判断来源集合是不是 AbstractImmutableList 类型的,如果是,就直接返回,如果不是,则调用 of 创 建一个新的集合。

示例2因为用的 new 创建的集合,不属于不可变 AbstractImmutableList 类的子类,
所以 copyOf 方法又创建了一个新的实例,所以为false。

注意:使用of和copyOf创建的集合为不可变集合,不能进行添加、删除、替换、 排序等操作,不然会报 java.lang.UnsupportedOperationException 异常。
上面演示了 List 的 of 和copyOf 方法,Set 和Map 接口都有。

Java 11 新特性

  • 新增了一系列字符串处理方法

  • Optional 加强
    Optional 也增加了几个非常酷的方法,现在可以很方便的将一个 Optional 转换 成一个 Stream, 或者当一个空 Optional 时给它一个替代的。

  • 局部变量类型推断升级
    在var上添加注解的语法格式,在jdk10中是不能实现的。在JDK11中加入了这样的语法。

    1
    2
    3
    4
    5
    6
    7
    //错误的形式: 必须要有类型, 可以加上var
    //Consumer<String> con1 = (@Deprecated t) ->
    System.out.println(t.toUpperCase());

    //正确的形式:
    //使用var的好处是在使用lambda表达式时给参数加上注解。
    Consumer<String> con2 = (@Deprecated var t) -> System.out.println(t.toUpperCase());
  • 全新的HTTP 客户端API

    • HTTP,用于传输网页的协议,早在1997年就被采用在目前的1.1版本中。直 到2015年,HTTP2才成为标准。
    • HTTP/1.1和HTTP/2的主要区别是如何在客户端和服务器之间构建和传输数据。 HTTP/1.1依赖于请求/响应周期。 HTTP/2允许服务器“push”数据:它可以发 送比客户端请求更多的数据。这使得它可以优先处理并发送对于首先加载 网页至关重要的数据。
    • 这是 Java 9 开始引入的一个处理 HTTP 请求的的 HTTP Client API,该 API 支持同步和异步,而在 Java 11 中已经为正式可用状态,你可以在
    • java.net 包中找到这个 API。
      它将替代仅适用于blocking模式的HttpURLConnection(HttpURLConnection是在HTTP 1.0的时代创建的,并使用了协议无关的 方法),并提供对WebSocket 和 HTTP/2的支持。
  • 更简化的编译运行程序
    看下面的代码。

    1
    2
    3
    4
    5
    // 编译
    javac Javastack.java

    // 运行
    java Javastack

    在我们的认知里面,要运行一个 Java 源代码必须先编译,再运行,两步执行动作。而在未来的Java 11版本中,通过一个 java 命令就直接搞定了,如以下所示:

    1
    java Javastack.java

    一个命令编译运行源代码的注意点:

  1. 执行源文件中的第一个类, 第一个类必须包含主方法。
  2. 并且不可以使用其它源文件中的自定义类, 本文件中的自定义类是可以使用的。
  • 废弃Nashorn引擎
    废除Nashorn javascript引擎,在后续版本准备移除掉,有需要的可以考虑使用GraalVM。

  • ZGC

    • GC是java主要优势之一。 然而, 当GC停顿太长, 就会开始影响应用的响应时 间。消除或者减少GC停顿时长, java将对更广泛的应用场景是一个更有吸引力 的平台。此外, 现代系统中可用内存不断增长,用户和程序员希望JVM能够以高 效的方式充分利用这些内存, 并且无需长时间的GC暂停时间。
    • ZGC, A Scalable Low-Latency Garbage Collector(Experimental) ZGC, 这应该是JDK11最为瞩目的特性, 没有之一。 但是后面带了Experimental, 说明这还不建议用到生产环境。
    • ZGC是一个并发, 基于region, 压缩型的垃圾收集器, 只有root扫描阶段会 STW(stop the world), 因此GC停顿时间不会随着堆的增长和存活对象的增长 而变长。
  • 优势:

    • GC暂停时间不会超过10ms
    • 既能处理几百兆的小堆, 也能处理几个T的大堆(OMG)
    • 和G1相比, 应用吞吐能力不会下降超过15%
    • 为未来的GC功能和利用colord指针以及Load barriers优化奠定基础
    • 初始只支持64位系统
  • ZGC的设计目标是:
    支持TB级内存容量,暂停时间低(<10ms),对整个 程序吞吐量的影响小于15%。 将来还可以扩展实现机制,以支持不少令人 兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。

  • 其它新特性

    • Unicode 10
    • Deprecate the Pack200 Tools and API
    • 新的Epsilon垃圾收集器
    • 完全支持Linux容器(包括Docker)
    • 支持G1上的并行完全垃圾收集
    • 最新的HTTPS安全协议TLS 1.3
    • Java Flight Recorder