java学习笔记2

java基础的后半部分

文章目录

一、多线程

JVM中每个线程都会有一个程序计数器来保存该线程执行到了哪个地方,所以当CPU再次调用该线程的时候就会从程序计数器保存的那个地方开始执行

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

1.1 概念

​ 进程:作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
​ 线程:进程进一步细化为线程,是一个程序内部的一条执行路径(打开一个软件,同时执行不同功能)

​ 若一个进程同一时间并行执行多个线程,就是支持多线程的
​ 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小

​ 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量
​ 和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患

方法区和堆:1个进程1份
虚拟机栈和程序计数器pc:1个线程1份
​ 多个线程共享1个进程当中的方法和堆

1.2单核cpu和多核cpu

​ 单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务
​ 例如,虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就
​ 好比收费人员,如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备
​ 好了钱,再去收费),但是因为CPU时间单元特别短,因此感觉不出来
​ 多核CPU:同一时间做多个事,能更好的发挥多线程的效率(现在的服务器都是多核的)

​ 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程
​ 如果发生异常,会影响主线程

​ 并行:多个cpu同时执行多个任务
​ 并发:一个CPU(采用时间片)同时执行多个任务(比如秒杀)

1.3多线程程序的优点

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

1.4何时需要多线程

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

2、线程的创建和使用

2.1 多线程概念

​ 当程序能一条线出来时,不是多线程,要多条路径才能执行完程序才是多线程
​ Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现
​ JDK1.5之前创建新执行线程有两种方法:
​ (1) 继承Thread类的方式
​ (2) 实现Runnable接口的方式

2.2 Thread类的特性

​ (1) 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
​ (2) 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
​ (3) Thread():创建新的Thread对象
​ Thread(String threadname):创建线程并指定线程实例名
​ Thread(Runnabletarget):指定创建线程的目标对象,它实现了Runnable接口中的run方法
​ Thread(Runnable target, String name):创建新的Thread对象

2.3 创建多线程的方式

​ 创建多线程的方式一:继承Thread类
​ ①创建一个继承于Thread类的子类
​ ②重写Thread的run()方法 —> 将此线程的方法声明在run()中
​ ③创建Thread类的子类的对象
​ ④通过此对象调用start()

​ 创建多线程的方式二:实现Runnable接口
​ ①创建一个实现了Runnable接口得类
​ ②实现类去实现Runnable中的抽象方法:run()
​ ③创建实现类的对象
​ ④将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
​ ⑤通过Thread类的对象调用start()

方式一
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
    //2.重写Thread类的run()  run()方法里的内容即本线程要做的事
    @Override
    public void run() {
        for(int i = 1;i < 100;i++){
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

class MyThread2 extends Thread{
    //2.重写Thread类的run()
    @Override
    public void run() {
        for(int i = 1;i < 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+i+"第二个线程");
            }                                  //返回此线程名称
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {     //主线程
        //3.创建Thread类的子类的对象
        MyThread t1 = new MyThread();
        MyThread2 t2 = new MyThread2();
        MyThread t3 = new MyThread();  //再启动一个线程,实现和t1一样的效果
        //4.通过此对象调用start(),start()的作用:①启动当前线程 ②调用当前线程的run()
        t1.start();    //t1线程开始运行
        t2.start();    //t2线程开始运行
        //如果不调用start()方法而是直接调用run()方法,则就是正常调用类的方法,还是在main主线程中进行           的,没有启动多线程
        
        
        //创建Thread类的匿名子类的方式  直接等于MyThread t1 = new MyThread()和t1.start()这两句
        new Thread(){              //并且不用写继承Thread的子类
            @Override
            public void run() {
                for(int i = 0;i < 100;i++){
                    if(i % 2 == 0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
        
        Thread.currentThread().setName("主线程");  //给主线程命名
        
        //如下操作仍在main线程中执行的
        for(int i = 1;i < 100;i++){ //主线程、t1线程、t2线程随机交替运行,每次运行的结果都是随机的
            if(i % 2 == 0){
                System.out.println(i + "***main()***");
            }
        }
    }
}


方式二
//1.创建一个实现了Runnable接口得类
class MThread implements Runnable{

    //2.实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }                      //不可直接写getName()了,因为继承的不是Thread类,而是实现接口
        }                  
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        //3.创建实现类的对象
        MThread m1 = new MThread();    //n个线程共用同一个m1
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(m1);
        //5.通过Thread类的对象调用start():①启动线程 ②调用当前线程的run()->调用了Runnable类型的             //  target的run()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(m1);
        t2.setName("线程2");
        t2.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fWXZCXJO-1645788496308)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\image-20220214110837500.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tAoCDQMZ-1645788496310)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\04e20e90637405c92469045cac1fbb54.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vkxm35b5-1645788496313)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\4e851e2caa89df9b9838a273ad69249d.png)]

2.4 Thread类的有关方法

​ ① void start():启动当前线程;执行当前线程的run()
​ ② run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
​ ③ static Thread currentThread():静态方法,返回当前代码执行的线程,在Thread子类中就是this,通常
​ 用于主线程和Runnable实现类
​ ④ String getName():获取当前线程的名字
​ ⑤ void setName(String name):设置当前线程的名字
​ ⑥ static void yield():线程让步,释放当前CPU的执行权。暂停当前正在执行的线程,把执行机会让给优先
​ 级相同或更高(例如main线程)的线程。若队列中没有同优先级的线程,则忽略此方法
​ ⑦ thread子类.join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,
​ 线程a才结束阻塞状态
​ ⑧ static void sleep(long millitime):让当前线程“睡眠”指定时间的millitime毫秒(即在此段时间内放弃对cpu
​ 的控制权),在指定的millitime毫秒时间内,当前线程是阻塞状态的,
​ 使其他线程有机会被执行,时间到后重新排队
​ 抛出InterruptedException异常
​ ⑨ stop():已过时,当执行此方法时,强制结束当前线程
​ ⑩ boolean isAlive():返回boolean,判断线程是否还活着

2.5 线程的调度

​ Java的调度方法:同优先级线程组成先进先出队列(先到先服务),使用时间片策略
​ 对高优先级,使用优先调度的抢占式策略(只是概率高了而已,并不是一定)

​ 线程创建时继承父线程的优先级
​ 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用,并不意味着只有当高优先级的
​ 线程执行完以后,低优先级的线程才会被执行

   线程的优先级等级
     - MAX_PRIORITY:10
     - MIN _PRIORITY:1
     - NORM_PRIORITY:5 --->默认优先级
   涉及的方法
     - getPriority() :返回线程优先值
     - setPriority(intnewPriority) :改变线程的优先级
 
class HelloThread extends Thread {
    @Override
    public void run() {
        for (int j = 0; j < 100; j++) {

//            try {
//                sleep(10);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }

            if (j % 2 == 0) {
                System.out.println(getName() + ":" + getPriority() + ":" + j);
            } //继承了父类Thread,可以直接调用方法,不用再用Thread.currentThread().xx返回当前线程
        }
    }
    public HelloThread(String name){   //给线程命名需要重写构造器
        super(name);
    }
}

public class ThreadModeTest {
    public static void main(String[] args) {
        HelloThread h2 = new HelloThread("Thread : 1"); //给线程命名
        h2.start();

        //设置分线程的优先级
        h2.setPriority(Thread.MAX_PRIORITY);

        //给主线程命名
        Thread.currentThread().setName("主线程");
        Thread.currentThread().setPriority((Thread.MIN_PRIORITY));

        for(int j = 0;j < 100; j++){
            if(j % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + j);
            }

//            if(j == 20){
//                try {
//                    h2.join();
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//            }
        }

        System.out.println(h2.isAlive());
    }
}

2.6 多窗口卖票

 例子:创建三个c窗口卖票,总票数为100张
      存在线程的安全问题,待解决。
 
class Windows extends Thread{     //窗口是一个线程子类,这个窗口同时多个线程,因为卖票是一样的动作,
    private static int ticket = 100;                                  //所以用一样的run方法
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为: " + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

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

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

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

方式二卖票
class Windows1 implements Runnable{    
    private int ticket = 100;  //不用加static,自然会共用100张
    
    @Override
    public void run() {
        while(true){
           if(ticket > 0){
              System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
              ticket--;
           }else{
               break;
           }
        }
    }
}

public class WindowsTest1 {
    public static void main(String[] args) {
        window1 w = new window1();
        Thread t1 = new Thread(w);  //三个线程用同一个对象,故用同一个ticket
        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();
    }
}


 *  比较创建线程的两种方式。
 *  开发中:优先选择:实现Runnable接口的方式
 *  原因:1. 实现的方式没有类的单继承性的局限性(方式一使子类不能继承它的父类只能继承Thread了)
 *       2. 实现的方式更适合来处理多个线程有共享数据的情况
 *  
 *  联系:public class Thread implements Runnable
 *  相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()

2.7 线程的分类

​ Java中的线程分为两类:一种是守护线程(垃圾回收线程),一种是用户线程(main主线程)
​ (1) 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
​ (2) 守护线程是用来服务用户线程的,通过在start()方法前调用**thread.setDaemon(true)**可以把一个用户
​ 线程变成一个守护线程
​ (3) Java垃圾回收就是一个典型的守护线程。
​ (4) 若JVM中都是守护线程,当前JVM将退出。
​ (5) 形象理解:兔死狗烹,鸟尽弓藏(用户线程结束,则守护线程必结束)

3、线程的生命周期

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

​ (1)新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
​ (2)就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的
​ 条件,只是没分配到CPU资源
​ (3)运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
​ (4)阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入
​ 阻塞状态
​ (5)死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EngVk0k-1645788496314)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\b29133ad93d3839d259de57bbfa1397a.png)]

4、线程的同步(设定了执行的前后顺序)

4.1 火车窗口卖票的安全问题

​ 火车窗口卖票,当if(ticket>0)时,输出卖票的前面加一行sleep(100),则可多个线程同时进入if选择,同时卖出
​ 同一张票,存在很大的安全问题
​ 极端情况,在ticket=1时三个进程进入if,此时sleep睡眠阻塞,等唤醒后,ticket变成了1,0,-1
​ 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票

4.2 在java中通过同步(synchronized)机制来解决线程的安全问题(上锁)

​ (1)方式一:同步代码块
​ synchronized(同步监视器){
​ //需要被同步的代码
​ }
​ 说明:操作共享数据的代码,即为需要被同步的代码 —>不能包含代码多了,也不能包含代码少了
​ 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据
​ 同步监视器,俗称:锁。任何一个类的对象,都可以来充当锁
​ 要求:多个线程必须要共用同一把锁(唯一的对象)
​ 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器
​ 在继承Thread类创建多线程的方式中,慎用this
​ (2)方式二:同步方法
​ 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的
​ (3)好处:同步的方式,解决了线程的安全问题
​ 局限:操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低

同步代码块处理实现Runnable接口的线程安全问题
class Windows1 implements Runnable{

    private int ticket = 100;
//  Object obj = new Object();   public的
//  Dog dog = new Dog();  任何一个类的对象都可当锁

    @Override
  public void run() {
      //不可把Object obj = new Object()放这,因为这样会1个线程1个锁,就不能共用锁了
       while(true){    
           synchronized (this) {//此时的this:唯一的windows1的对象 //方式二:synchronized (dog) 
                if (ticket > 0) { //用了this则不用再obj或者dog这种创建类对象
                    try{
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
//main方法同买票的


同步代码块处理继承Thread类的线程安全问题
class Windows extends Thread{
    private static int ticket = 100;
    private static Object obj = new Object(); //必须要加static,不然一个线程会创建一个obj对象

    @Override
    public void run() {
        while(true){
          //synchronized (obj) {  这么写是正确的                         是唯一的,只会加载一次
            synchronized (Windows.class){ //Class clazz = Windows.class类也是对象 方便的写法
          //synchronized (this) { 这么写是错误的,因为此时this表示的是t1,t2,t3三个对象,故锁不唯一
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ":卖票,票号为: " + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}    
//main方法同买票的   
    
同步方法处理实现Runnable接口的线程安全问题
class Windows3 implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();  //把同步的代码块替换成一个方法拎出来,将方法声明为synchronized同步的
        }
    }

    public synchronized void show() { //同步监视器:this
        //synchronized (this){
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
                ticket--;
            }
        //}
    }
}
//main方法同买票的    
    
同步方法处理继承Thread类的线程安全问题
class Windows4 extends Thread {
    private static int ticket = 100;  //多个静态的
    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private static synchronized void show(){//同步监视器:Window4.class 多个静态的
        //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}
//main方法同买票的    

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

使用同步机制将单例模式中的懒汉式改写为线程安全的   (用同步代码块)
    
public class BankTest { }
class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance(){
        //方式一:效率稍差                         
        //快捷键:Alt+Shift+Z
//        synchronized (Bank.class) {     所有线程都进去了,发现空的再出来
//            if(instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }

        //方式二:效率较高
        if(instance == null) {       //只有不为空的时候才会进去,不用像方式一一样白跑一趟
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

4.4 死锁问题

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

出现死锁
public class ThreadTest {                              正常输出两种结果cd   或   ab
    public static void main(String[] args) {                        34        12
                                                                    cdab      abcd
        StringBuffer s1 = new StringBuffer();                       3412      1234
        StringBuffer s2 = new StringBuffer();

        new Thread(){       //继承Thread类的匿名子类
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);   //加了sleep,故死锁出现的概率大大增加
                    } 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() {   //实现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();
    }
}

出现死锁
class A {
	public synchronized void foo(B b) {  //此时的锁是A类的对象:a
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {
		System.out.println("进入了A类的last方法内部");
	}
}
class B {
	public synchronized void bar(A a) { //锁是b
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {  //锁是b
		System.out.println("进入了B类的last方法内部");
	}
}
public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();
	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}
	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}
	public static void main(String[] args) {
		DeadLock dl = new DeadLock();  //这两行是调用分线程,因为run方法
		new Thread(dl).start();
		dl.init();   //这是主线程
	}
}

4.5 解决线程安全问题的方式三:lock锁 (jdk5.0新增的) (前两种:同步代码区、同步)

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

import java.util.concurrent.locks.ReentrantLock;

 * 注意:如果同步代码有异常,要将unlock()写入finally语句块
 *
 * 1. 面试题:synchronized(同步)Lock()的异同?
 *    相同:二者都可以解决线程安全问题
 *    不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
 *         Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock()*
 * 2.优先使用顺序:
 *      Lock->同步代码块(已经进入了方法体,分配了相应资源)->同步方法(在方法体之外)
 
   面试题:如何解决线程安全问题?有几种方式
          3种,同步代码块,同步方法,lock锁
     
class Windows implements Runnable{

    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();  //1.实例化ReentrantLock

    @Override
    public void run() {
        while(true){
            try{
                lock.lock();  //调用锁定方法:lock()
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":售票,票号为: " + ticket);
                    ticket --;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}
//下面main方法同买票
/**
 * 银行有一个账户。
 * 有两个储户分别向同一个账户存3000元,每次存1000,存3次。
 * 每次存完打印账户余额。
 *
 * 分析:
 *      1.是否是多线程问题?是,两个储户线程
 *      2.是否有共享数据?有,账户(或账户余额)
 *      3.是否有线程安全问题?有(线程安全问题取决于是否有共享数据)
 *      4.需要考虑如何解决线程安全问题?同步机制:有三种方式
 */
class Account{
    private double balance;
    public Account(double balance){
        this.balance = balance;
    }
    //存钱
    public synchronized void deposit(double amt){
        if(amt > 0){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance += amt;
            System.out.println(Thread.currentThread().getName() + ":" + "存钱成功,当前余额:" + balance);
        }
    }
}
class Customer extends Thread{
    private Account acct;
    public Customer(Account acct){
        this.acct = acct;
    }
    @Override
    public void run() {
        for(int i = 0;i < 3;i++){
            acct.deposit(1000);
        }
    }
}
public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account(0);
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

线程是否安全取决于是否有共享数据

5、线程的通信(即几个方法的使用)

5.1 线程通信涉及到的方法

​ (1)wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器**(执行了wait就会释放锁)**
​ notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个
​ notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
​ (2)wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中
​ wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现
​ IllegalMonitorStateException异常
​ wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
​ (3)sleep() 和 wait()的异同?
​ 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
​ 不同点:①两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
​ ②调用的要求不同:sleep()可以在任何需要的场景下调用
​ wait()必须使用在同步代码块或同步方法中
​ ③关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放
​ 锁,wait()会释放锁

线程通信的例子:使用两个线程打印1-100。线程1, 线程2 交替打印
    
class Number implements Runnable{
    private int number = 1;
    public Object obj = new Object();
    @Override
    public void run() {
        while (true){
            
            synchronized (obj) {
                obj.notify();
                if(number <= 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

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

5.2 生产者/消费者问题

 * 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
 * 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,
 * 店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;
 * 如果店中没有产品了,店员会告诉消费者等一下,
 * 如果店中有产品了再通知消费者来取走产品。
 *
 * 分析:
 *      1.是否是多线程的问题?是,生产者的线程,消费者的线程
 *      2.是否有共享数据的问题?是,店员、产品、产品数
 *      3.如何解决线程的安全问题?同步机制,有三种方法
 *      4.是否涉及线程的通信?是
 */

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(getName() + ": 开始生产产品......");
        while(true){
            try {
                Thread.sleep(10);
            } 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(getName() + ": 开始消费产品......");
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}

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

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

        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        Consumer c2 = new Consumer(clerk);
        c2.setName("消费者2");

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

6、JDK5.0新增线程创建方式

6.1 创建多线程的方式三:实现Callable接口
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
(1)相比run方法(),call()可以有返回值的
(2)call()可以抛出异常,被外面的操作捕获,获取异常的信息
(3)支持泛型的返回值
(4)需要借助FutureTask类,比如获取返回结果
Future接口:可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
FutrueTask是Futrue接口的唯一的实现类
FutureTask同时实现了Runnable, Future接口,它既可以作为Runnable被线程执行,又可以作
为Future得到Callable的返回值

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1.创建一个实现Callable的实现类
class NumThread implements Callable{

    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 1;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

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

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

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

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

6.2 创建多线程的方式四:线程池(新增方式之二)
(1)背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中
可以避免频繁创建销毁、实现重复利用,类似生活中的公共交通工具
好处:提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理--------corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime`:线程没有任务时最多保持多长时间后会终止
(2)线程池相关API
JDK 5.0起提供了线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者
定期地执行。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

面试题:创建多线程有几种方式?四种!

class NumberThread implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {

        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());  //适合适用于Runable
        service.execute(new NumberThread1());  //适合适用于Runable
      //service.submit(Callable callable);   //适合适用于Callable

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

二、常用类

1、字符串相关的类

String类、StringBuffer类、StringBuilder类

1.1 String类

​ (1)String类概述
​ Java程序中的所有字符串字面值(如“abc”)都作为此类的实例实现
​ String声明为final的,不可被继承,代表不可变的字符序列(常量池里的绝对不可变,修改就新开辟
​ String内部定义了final char[] value用于存储字符串数据
​ String实现了Serializable接口:表示字符串是支持序列化的(可通过网络方式传给对方)
​ 实现了Comparable接口:表示String可以比较大小
​ (2)理解String的不可变性(String代表不可变的字符序列)
​ ①当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
​ ②当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
​ ③当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有
​ 的value进行赋值
​ ④通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
​ ⑤方法区中的字符串常量池中是不会存储相同内容的字符串的(故下面的s1和s2指向一个地址)
​ (3)String的实例化方式
​ 方式一:通过字面量定义的方式
​ 方式二:通过new+构造器的方式

   String的不可变性指字符串值只要有任何修改,都会开辟一个新地址存放修改后的,且此时字符串指向新地址
   public void Test1(){
        String s1 = "abc";  //字面量的定义方式(此时的字符串值声明在字符串常量池中)
        String s2 = "abc";  //此时s1和s2指向一个地址
        s1 = "hello"; //对字符串重新赋值时,方法区新开辟出一个地址存放新的赋值,字符串地址指向这个新地址

        System.out.println(s1 == s2);//比较s1和s2的地址值

        System.out.println(s1);//hello
        System.out.println(s2);//abc

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

        String s3 = "abc";
        s3 += "def"; //现有字符串后面拼接一个内容,也要新造一个地址存放拼接后完整的字符串,此时字符串
        System.out.println(s3);//abcdef                                         指向新地址

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

        String s4 = "abc";
        String s5 = s4.replace('a','m');
        System.out.println(s4);//abc
        System.out.println(s5);//mbc
   }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ksePq0eL-1645788496317)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\20200509201648472.png)]

​ (3)String不同实例化方式的对比
​ ①String对象的创建

   String str = "hello";

  //本质上this.value = new char[0];  value字符串的内容
  String  s1 = new String(); 

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3E2RupuB-1645788496318)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\20200509201719650.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3ZBG2r7-1645788496319)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\20200509201719569.png)]

​ ②面试题:String str1 = “abc”;与String str2 = new String(“abc”);的区别?
​ 答:字符串常量存储在字符串常量池,目的是共享
​ 字符串非常量对象存储在堆中

import org.junit.Test;
public class StringTest {

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

    public void test2(){
        //通过字面量定义的方式:此时的s1和s2的数据javaEE声明在方法区中的字符串常量池中。
        String s1 = "javaEE";
        String s2 = "javaEE";

        //通过new + 构造器的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。
        String s3 = new String("javaEE");
        String s4 = new String("javaEE");

        System.out.println(s1 == s2);//true(常量在方法区的位置一样)
        System.out.println(s1 == s3);//false
        System.out.println(s1 == s4);//false
        System.out.println(s3 == s4);//false

        System.out.println("***********************");
        Person p1 = new Person("Tom",12);
        Person p2 = new Person("Tom",12);

        System.out.println(p1.name.equals(p2.name));//true
        System.out.println(p1.name == p2.name);//true String类型的比较了地址,因为是“Tom”这种
                                               //字面量定义的方式,所以指向字符串常量,为同一地址
        p1.name = "Jerry";
        System.out.println(p2.name);//Tom
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wg92Nq0R-1645788496319)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\20200509201810827.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B13pN7uh-1645788496320)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\20200509201833464.png)]

​ (4)String不同拼接操作的对比
​ ①常量与常量的拼接结果在常量池,且常量池中不会存在相同内容的常量
​ ②只要其中有一个是变量,结果就在堆中(只要有一个变量,则就要new一个)
​ ③如果拼接的结果调用intern()方法,返回值就在常量池中

public class StringTest {     //若是变量为final的,则其变成了常量,和常量池的常量一个效果
    @Test
    public void test4(){
        String s1 = "javaEEhadoop";
        String s2 = "javaEE";
        String s3 = s2 + "hadoop";
        System.out.println(s1 == s3);//false

        final String s4 = "javaEE";//s4:常量
        String s5 = s4 + "hadoop";
        System.out.println(s1 == s5);//true

    }
    public void test3(){
        String s1 = "javaEE";
        String s2 = "hadoop";

        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;

        System.out.println(s3 == s4);//true  只有常量互相拼接才会地址还在方法区
        System.out.println(s3 == s5);//false 其他任何只要有一个变量,就会new在堆里得到新地址
        System.out.println(s3 == s6);//false
        System.out.println(s5 == s6);//false
        System.out.println(s3 == s7);//false
        System.out.println(s5 == s6);//false
        System.out.println(s5 == s7);//false
        System.out.println(s6 == s7);//false

        String s8 = s5.intern();//返回值得到的s8使用的常量值中已经存在的“javaEEhadoop”
        System.out.println(s3 == s8);//true
    }
}

//一道面试题
public class StringTest {
    String str = new String("good");
    char[] ch = { 't', 'e', 's', 't' };

    public void change(String str, char ch[]) {  //传的是地址
        str = "test ok";
        ch[0] = 'b';
    }
    public static void main(String[] args) {
        StringTest ex = new StringTest();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str);//good  String的不变性
        System.out.println(ex.ch);//best
    }
}

​ String s1 = “a”;
​ 说明:在字符串常量池中创建了一个字面量为"a"的字符串
​ s1 = s1 + “b”;
​ 说明:实际上原来的“a”字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+“b”(也就是"ab")
​ 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率
​ 如果这样的操作放到循环中,会极大影响程序的性能。
​ String s2 = “ab”;
​ 说明:直接在字符串常量池中创建一个字面量为"ab"的字符串
​ String s3 = “a” + “b”;
​ 说明:s3指向字符串常量池中已经创建的"ab"的字符串
​ String s4 = s1.intern();
​ 说明:堆空间的s1对象在调用intern()之后,会将常量池中已经存在的"ab"字符串赋值给s4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a0G4HF55-1645788496321)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\20200509201854255.png)]

1.2 JVM中涉及字符串的内存结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M0KOxCbk-1645788496322)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\4bf4b2285af70e363da6445fe73896c7.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QXGLQYrC-1645788496323)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\5a2f00eaeeb8d0ec04465632e551dca8.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P7yfJwCe-1645788496324)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\94b148a54b3c86ec101b9b88fc885067.png)] ! [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gT7WIzwh-1645788496325)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\aa3755823eee56483b00f06c1e905ec6.png)]

​ 永久区可看成方法区,只是为了规范将其划进堆里 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HMQtSvw2-1645788496326)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\d48e2620b1e1997102c86dfc932977ff.png)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1KngnRHk-1645788496327)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\c5c4dd11e2610077965445af6e822767.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nCtj8Yi5-1645788496327)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\83d0b732dff16d3852b75b44231f56b1.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7AqwqLtF-1645788496328)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\e71ddbdda5c97fd91a19e3538987a7a2.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IjC3yKv5-1645788496329)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\705c8dc581ebcd5279ac303302c88247.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JnQOHM3n-1645788496330)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\4a022a19ef84a70eb734285dd81340bd.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5s1Th3ZO-1645788496330)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\a1d069bfdf3b25e95a2b5e793a8cfab7.png)]

1.3 String的常用方法

  * int length():返回字符串的长度:return value.length
  * char charAt(int index):返回某索引处的字符return value[index]
  * boolean isEmpty():判断是否是空字符串:return value.length==0
  * String toLowerCase():使用默认语言环境,将String中的所有字符转换为小写
  * String toUpperCase():使用默认语言环境,将String中的所有字符转换为大写
  * String trim():返回字符串的副本,忽略前导空白和尾部空白
  * boolean equals(Object obj):比较字符串的内容是否相同
  * boolean equals IgnoreCase(String anotherString):与equals方法类似,忽略大小写
  * String concat(String str):将指定字符串连接到此字符串的结尾。等价于用“+* int compareTo(String anotherString):比较两个字符串的大小
  * String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到                                         最后的一个子字符串,0开始数
  * String substring(int beginIndex,int endIndex):返回一个新字符串,它是此字符串从beginIndex                                                      开始截取到endIndex(不包含)的一个子字符串
      
  
  * boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
  * boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
  * boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指                                                     定前缀开始
  * boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
  * int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
  * int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定                                             的索引开始
  * int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
  * int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,                                                 从指定的索引开始反向搜索
//    问:什么情况下,indexOf(str)和lastIndexOf(str)返回值相同?
//    答:情况一:存在唯一的一个str
//       情况二:不存在str
      
      
 替换:
  * String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字                    字符                          符串中出现的所有 oldChar 得到的
  * String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替                      字符串                            换此字符串所有匹配字面值目标序列的子字符串 
  * String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串                                                          所有匹配给定的正则表达式的子字符串
  * String replaceFirst(String regex, String replacement):使用给定的replacement 替换此字符                                                            串匹配给定的正则表达式的第一个子字符串
 匹配:
  * boolean matches(String regex):告知此字符串是否匹配给定的正则表达式
 切片:
  * String[] split(String regex):根据给定正则表达式的匹配拆分此字符串
  * String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过
                                             limit个,如果超过了,剩下的全部都放到最后一个元素中

      

import org.junit.Test;
public class StringMethodTest {
     public void Test1(){
        String s1 = "helloworld";
        System.out.println(s1.length());
        System.out.println(s1.length());
        System.out.println(s1.charAt(0));
        System.out.println(s1.charAt(9));
      //System.out.println(s1.charAt(10));
      //s1 = "";
        System.out.println(s1.isEmpty());

        String s2 = s1.toLowerCase();
        System.out.println(s1);//s1不可变的,仍然为原来的字符串
        System.out.println(s2);//改成小写以后的字符串

        String s3 = "   he  llo   world   ";
        String s4 = s3.trim();
        System.out.println("-----" + s3 + "-----");
        System.out.println("-----" + s4 + "-----");
    }
    
    public void test2(){
        String s1 = "HelloWorld";
        String s2 = "helloworld";
        System.out.println(s1.equals(s2));//false
        System.out.println(s1.equalsIgnoreCase(s2));//true
        
        String s3 = "abc";
        String s4 = s3.concat("def");
        System.out.println(s4);//abcdef

        String s5 = "abc";
        String s6 = new String("abe");
        System.out.println(s5.compareTo(s6));//-2   //涉及到字符串的排序 c比e小2

        String s7 = "周围好吵啊";
        String s8 = s7.substring(2);
        System.out.println(s7);
        System.out.println(s8);

        String s9 = s7.substring(0, 2);
        System.out.println(s9);
    }

    public void test3(){
        String str1 = "helloworld";
        boolean b1 = str1.endsWith("rld");
        System.out.println(b1);

        boolean b2 = str1.startsWith("He");
        System.out.println(b2);

        boolean b3 = str1.startsWith("ll",2);
        System.out.println(b3);

        String str2 = "wor";
        System.out.println(str1.contains(str2));

        System.out.println(str1.indexOf("lo"));

        System.out.println(str1.indexOf("lo",5));

        String str3 = "hellorworld";

        System.out.println(str3.lastIndexOf("or"));
        System.out.println(str3.lastIndexOf("or",6));
    }

    public void test4(){
        String str1 = "西藏布达拉宫欢迎您";
        String str2 = str1.replace('西','东');

        System.out.println(str1);
        System.out.println(str2);

        String str3 = str1.replace("北京", "南京");
        System.out.println(str3);

        System.out.println("*************************");
        String str = "12hello34world5java7891mysql456";
        //把字符串中的数字替换成,,如果结果中开头和结尾有,的话去掉
        String string = str.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
        System.out.println(string);      d表示数字 +表示1个或多个数字 ^表示开头 $表示结尾

        System.out.println("*************************");
        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);

        System.out.println("*************************");
        str = "hello|world|java";
        String[] strs = str.split("\\|");
        for (int i = 0; i < strs.length; i++) {
            System.out.println(strs[i]);
        }
        System.out.println();
        str2 = "hello.world.java";
        String[] strs2 = str2.split("\\.");
        for (int i = 0; i < strs2.length; i++) {
            System.out.println(strs2[i]);
        }
    }

1.4 String与基本数据类型包装类、char[ ]、byte[ ]的转换

(1)String与基本数据类型包装类的转换
String --> 基本数据类型、包装类:调用包装类的静态方法:parseXxx(str)
int num = Integer.parseInt(str1);
int num = (int)str1; 这是错误的
基本数据类型、包装类 --> String:调用String重载的valueOf(xxx)
String str2 = String.valueOf(num);
拼接运算String str3 = num + “”;
(2)String与char[ ]之间的转换
String --> char[ ]:调用String的toCharArray()
char[] charArray = str1.toCharArray();
char[ ] --> String:调用String的构造器
char[] arr = new char[]{‘h’,‘e’,‘l’,‘l’,‘o’};
String str2 = new String(arr);
(3)String与byte[ ]之间的转换(转成ASCII码即二进制数据)
编码:String --> byte[]:调用String的getBytes()
解码:byte[] --> String:调用String的构造器
编码:字符串 -->字节 (看得懂 —>看不懂的二进制数据)
解码:编码的逆过程,字节 --> 字符串 (看不懂的二进制数据 —> 看得懂)
说明:解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
    public void test3() throws UnsupportedEncodingException {
        String str1 = "abc123重工";   //1个汉字占2个字节,字符和数字占1个字节
        byte[] bytes = str1.getBytes();//使用默认的字符编码集,进行转换
        System.out.println(Arrays.toString(bytes));//[97,98,99,49,50,41] 97是1个字节

        byte[] gbks = str1.getBytes("gbk");//使用gbk字符集进行编码。
        System.out.println(Arrays.toString(gbks));

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

        String str2 = new String(bytes);//使用默认的字符集,进行解码。
        System.out.println(str2);

        String str3 = new String(gbks);
        System.out.println(str3);//出现乱码。原因:编码集和解码集不一致!

        String str4 = new String(gbks,"gbk");
        System.out.println(str4);//没有出现乱码。原因:编码集和解码集一致!
    }

1.5 面试中String算法考查的说明

模拟一个trim方法,去除字符串两端的空格 
public class StringExer {
	public String myTrim(String str) {
		if (str != null) {
			int start = 0;// 用于记录从前往后首次索引位置不是空格的位置的索引
			int end = str.length() - 1;// 用于记录从后往前首次索引位置不是空格的位置的索引

			while (start < end && str.charAt(start) == ' ') {
				start++;
			}

			while (start < end && str.charAt(end) == ' ') {
				end--;
			}
			if (str.charAt(start) == ' ') {
				return "";
			}

			return str.substring(start, end + 1);
		}
		return null;
	}
	
	@Test
	public void testMyTrim() {
		String str = "   a   ";
		// str = " ";
		String newStr = myTrim(str);
		System.out.println("---" + newStr + "---");
	}
}


将一个字符串进行反转,将字符串中指定部分进行反转。比如“abcdefg”反转为”abfedcg”
public class StringDemo {
    //方式一:转换为char[]
    public String reverse(String str,int startIndex,int endIndex){
        if(str != null && str.length() != 0) {
            char[] arr = str.toCharArray();
            for (int x = startIndex, y = endIndex; x < y; x++, y--) {
                char temp = arr[x];
                arr[x] = arr[y];
                arr[y] = temp;
            }
            return new String(arr);
        }
        return null;
    }

    //方式二:使用String的拼接
    public String reverse2(String str, int startIndex, int endIndex) {
        if(str != null) {
            // 第一部分
            String reverStr = str.substring(0,startIndex);// ab
            // 第二部分
            for (int i = endIndex; i >= startIndex; i--) {
                reverStr += str.charAt(i);
            } // abfedc
            // 第三部分
            reverStr += str.substring(endIndex + 1);
            return reverStr;
        }
        return null;
    }

    //方式三:使用StringBuffer/StringBuilder替换String
    public String reverse3(String str, int startIndex, int endIndex) {
        StringBuilder builder = new StringBuilder(str.length());
        if(str != null) {
            //第一部分
            builder.append(str.substring(0, startIndex));
            //第二部分
            for (int i = endIndex; i >= startIndex; i--) {
                builder.append(str.charAt(i));
            }
            //第三部分
            builder.append(str.substring(endIndex + 1));
            return builder.toString();
        }
        return null;
    }
    @Test
    public void testReverse() {
        String str = "abcdefg";
        String str1 = reverse3(str, 2, 5);
        System.out.println(str1);// abfedcg

    }
}

获取一个字符串在另一个字符串中出现的次数。比如:获取“ ab”在“abkkcadkabkebfkabkskab” 中出现的次数
public class StringDemo2 {
    public int getCount(String mainStr,String subStr){
        int mainLength = mainStr.length();
        int subLength = subStr.length();
        int count = 0;
        int index = 0;

        if(mainLength >= subLength){
            //方式一:
//            while((index = mainStr.indexOf(subStr)) != -1){
//                count++;
//                mainStr = mainStr.substring(index + subStr.length());
//            }
            //方式二:对方式一的改进
            while((index = mainStr.indexOf(subStr,index)) != -1){
                count++;
                index += subLength;
            }

            return count;
        }else{
            return 0;
        }
    }

    @Test
    public void testGetCount(){
        String mainStr = "abkkcadkabkebfkabkskab";
        String subStr = "ab";
        int count = getCount(mainStr,subStr);
        System.out.println(count);
    }
}


获取两个字符串中最大相同子串。比如:str1 = "abcwerthelloyuiodef“;str2 = “cvhellobnm”
提示:将短的那个串进行长度依次递减的子串与较长的串比较
import org.junit.Test;
import java.util.Arrays;
public class StringDemo3 {
    //前提:两个字符串中只有一个最大相同子串
    public String getMaxSameString(String str1,String str2){
        if(str1 != null && str2 != null){
            String maxStr = (str1.length() >= str2.length())? str1 : str2;
            String minStr = (str1.length() < str2.length())? str1 : str2;
            int length = minStr.length();

            for(int i = 0;i < length;i++){
                for(int x = 0,y = length - i;y <= length;x++,y++){
                    String subStr = minStr.substring(x,y);
                    if(maxStr.contains(subStr)){
                        return subStr;
                    }

                }
            }

        }
        return null;
    }
    // 如果存在多个长度相同的最大相同子串
    // 此时先返回String[],后面可以用集合中的ArrayList替换,较方便
    public String[] getMaxSameString1(String str1, String str2) {
        if (str1 != null && str2 != null) {
            StringBuffer sBuffer = new StringBuffer();
            String maxString = (str1.length() > str2.length()) ? str1 : str2;
            String minString = (str1.length() > str2.length()) ? str2 : str1;

            int len = minString.length();
            for (int i = 0; i < len; i++) {
                for (int x = 0, y = len - i; y <= len; x++, y++) {
                    String subString = minString.substring(x, y);
                    if (maxString.contains(subString)) {
                        sBuffer.append(subString + ",");
                    }
                }
//                System.out.println(sBuffer);
                if (sBuffer.length() != 0) {
                    break;
                }
            }
            String[] split = sBuffer.toString().replaceAll(",$", "").split("\\,");
            return split;
        }

        return null;
    }
    @Test
    public void testGetMaxSameString(){
        String str1 = "abcwerthello1yuiodefabcdef";
        String str2 = "cvhello1bnmabcdef";
        String[] maxSameStrings = getMaxSameString1(str1, str2);
        System.out.println(Arrays.toString(maxSameStrings));

    }
}


对字符串中字符进行自然顺序排序
1)字符串变成字符数组
2)对数组排序,选择,冒泡,Arrays.sort();
3)将排序后的数组变成字符串
    public class StringDemo4 {
	public void testSort() {
		String str = "abcwerthelloyuiodef";
		char[] arr = str.toCharArray();
		Arrays.sort(arr);

		String newStr = new String(arr);
		System.out.println(newStr);
	}
}

1.6 StringBuffer和StringBuilder

(1)String、StringBuffer、StringBuilder三者的异同?
String:不可变的字符序列;底层使用char[]存储(char[] value这个数组)
StringBuffer:可变的字符序列;线程安全的(内部都是同步方法),故效率低;底层使用char[]存储 是多线程
StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储 不是多线程
(2)StringBuffer的源码分析
str.length返回的是StringBufffer内部的count数,而只有str.append才会增加count数,即字符数
StringBuffer内部初始默认的char数组的长度16,指的是StringBuffer内部value数组的长度

扩容问题:如果要添加的数据底层数组(value)盛不下了,那就需要扩容底层的数组(value)
​ 默认情况下,扩容为原来容量的2倍+2(即将value数组的长度value.length*2+2),同时将原有数组中的元素
​ 复制到新的数组中(StringBuffer类源码里有)

​ 故开发中建议使用:StringBuffer(int capacity) 或 StringBuilder(int capacity)指定长度,这样就不用扩容了

public class StringBufferBuilderTest {

     * String str = new String();//char[] value = new char[0]; 构造器无参数则创建char[0]
     * String str1 = new String("abc");//char[] value = new char[]{'a','b','c'};
     * StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];
                                             //底层创建了一个长度是16的数组
     * System.out.println(sb1.length());//0 length返回的是count数,而只有append才会增加count数
     * sb1.append('a');//value[0] = 'a';    16指的是字符串内部value的长度,而不是字符串的长度
     * sb1.append('b');//value[1] = 'b';
     *
     StringBuffer sb2=new StringBuffer("abc");//char[] value=new char["abc".length()+16];
     *
     * 问题1.System.out.println(sb2.length());//3
     * 问题2.扩容问题:如果要添加的数据底层数组(value)盛不下了,那就需要扩容底层的数组(value)
     *              默认情况下,扩容为原来容量的2+2(即将value数组的长度value.length*2+2),同时将                     原有数组中的元素复制到新的数组中(StringBuffer类源码里有)
     *
     * 意义:开发中建议大家使用:StringBuffer(int capacity)StringBuilder(int capacity)
     *      指定长度,这样就不用扩容了
     */
    @Test
    public void test1(){
        StringBuffer sb1 = new StringBuffer("abc");
        sb1.setCharAt(0,'m');
        System.out.println(sb1);

        StringBuffer sb2 = new StringBuffer();
        System.out.println(sb2.length());   //0
    }
}

(3)StringBuffer中的常用方法
StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接 和String类的最大区别
StringBuffer delete(int start,int end):删除指定位置的内容
StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
StringBuffer insert(int offset, xxx):在指定位置插入xxx
StringBuffer reverse() :把当前字符序列逆转
public int indexOf(String str):索引,返回该字符的位置
public String substring(int start,int end):返回一个从start开始到end索引结束的左闭右开区间的子字符串
public int length()
public char charAt(int n ):返回该位置的字符
public void setCharAt(int n ,char ch):将指定位置字符改成新的

​ 增:append(xxx)
​ 删:delete(int start,int end)
​ 改:setCharAt(int n ,char ch) / replace(int start, int end, String str)
​ 查:charAt(int n )
​ 插:insert(int offset, xxx)
​ 长度:length();
​ 遍历:for() + charAt() / toString()
(4)String、StringBuffer、StringBuilder效率对比
​ 效率从高到低排列:StringBuilder > StringBuffer > String(前两个数量级差不多)

2、JDK 8之前的日期时间API

System静态方法、Date类、Calendar类、SimpleDateFormat类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eYgFfkJu-1645788496332)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\01409d8ae2e27491b95fb69d861520ee.png)]

2.1 System类中获取时间戳的方法

​ System类提供的public static long currentTimeMillis()用来返回当前时间与1970年1月1日0时0分0秒之间
​ 以毫秒为单位的时间差
​ long time = System.currentTimeMillis(); //time称为时间戳

2.2 Java中两个Date类的使用

(1)java.util.Date类—>表示特定的瞬间,精确到毫秒
java.sql.Date类—>数据库中有个类型叫date,为了和java中的Date匹配,创建出了这个数据库Date
(2)两个构造器的使用
构造器一:Date():创建一个对应当前时间的Date对象
构造器二:创建指定毫秒数的Date对象
(3)两个方法的使用
toString():显示当前的年、月、日、时、分、秒
getTime():获取当前Date对象对应的毫秒数。(时间戳)
(4)java.sql.Date对应着数据库中的日期类型的变量
如何实例化
如何将java.util.Date对象转换为java.sql.Date对象

public class DateTimeTest {
    public void test2(){
        //构造器一:Date():创建一个对应当前时间的Date对象
        Date date1 = new Date();
        System.out.println(date1.toString()); //Sat May 09 20:09:11 CST 2020 这里只显示到秒
        System.out.println(date1.getTime());  //1589026216998 毫秒为单位
        //偏移量
        Date date1 = new Date(2020,9,8);
        System.out.println(date1); //输出Fri Oct 08 00:00:00 CST 3920,因为1900,0月0号为起点
        Date date2 = new Date(2020 - 1900,9 - 1,8);
        System.out.println(date2); //输出Tue Sep 08 00:00:00 CST 2020,这才是所设定的时间
        
        //构造器二:创建指定毫秒数的Date对象
        Date date2 = new Date(1589026216998L);
        System.out.println(date2.toString());

        //创建java.sql.Date对象
        java.sql.Date date3 = new java.sql.Date(35235325345L);
        System.out.println(date3);  //1971-02-13

        //如何将java.util.Date对象转换为java.sql.Date对象
        //情况一:
//        Date date4 = new java.sql.Date(2343243242323L);
//        java.sql.Date date5 = (java.sql.Date) date4;   强转只能在多态下使用
        //情况二:
        Date date6 = new Date();
        java.sql.Date date7 = new java.sql.Date(date6.getTime());
    }
}

强转只能在多态后使用

2.3 SimpleDateFormat的使用

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

​ 它允许进行 格式化:日期—>文本
​ 解析:文本—>日期
(2)SimpleDateFormat的使用:SimpleDateFormat对日期Date类的格式化和解析
​ ①两个操作
​ 格式化:日期—>字符串 用sdf.format(date)
​ 解析:格式化的逆过程,字符串—>日期 用sdf.parse(str)
​ ②SimpleDateFormat的实例化

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

public class DateTime {
    public void testSimpleDateFormat() throws ParseException {
        //实例化SimpleDateFormat
        SimpleDateFormat sdf = new SimpleDateFormat();  //构造器空参,则默认格式

        //格式化:日期--->字符串 用sdf.format(date)
        Date date = new Date();
        System.out.println(date);   //Sun May 10 16:34:30 CST 2020

        String format = sdf.format(date);
        System.out.println(format); //20-5-10 下午4:34

        //解析:格式化的逆过程,字符串--->日期 用sdf.parse(str)
        String str = "19-12-18 上午11:43";  /若是写的字符串格式不对,则会抛出ParseException异常
        Date date1 = sdf.parse(str);
        System.out.println(date1);  //Wed Dec 18 11:43:00 CST 2019

      //按照指定的方式格式化和解析:调用带参的构造器,日期格式化的格式参数指定了
      //SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyy.MMMMM.dd GGG hh:mm aaa");
        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyy.MMMMM.dd GGG hh:mm aaa");
        //格式化
        String format1 = sdf1.format(date);
        System.out.println(format1);    //02020.五月.10 公元 04:32 下午
        //解析:要求字符串必须是符合SimpleDateFormat识别的格式(通过构造器参数体现),
        //否则,抛异常
        Date date2 = sdf1.parse("02020.五月.10 公元 04:32 下午");
        System.out.println(date2);  //Sun May 10 16:32:00 CST 2020
    }
}

2.4 Calendar日历类的使用

Calendar是一个抽象基类,主用用于完成日期字段之间相互操作的功能
(1)获取Calendar实例的方法
使用Calendar.getInstance()方法
调用它的子类GregorianCalendar的构造器。
(2)一个Calendar的实例是系统时间的抽象表示,通过get(intfield)方法来取得想要的时间信息
比如YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、MINUTE、SECOND
public void set(intfield,intvalue)
public void add(intfield,intamount)
public final Date getTime()
public final void setTime(Date date)
(3)注意:
获取月份时:一月是0,二月是1,以此类推,12月是11
获取星期时:周日是1,周二是2,。。。。周六是7

import java.util.Calendar;
import java.util.Date;
import org.junit.Test;

public class DateTime {

    public void testCalendar(){
        //1.实例化
        //方式一:创建其子类(GregorianCalendar)的对象
        //方式二:调用其静态方法getInstance()
        Calendar calendar = Calendar.getInstance();
      //System.out.println(calendar.getClass());//得到的当前类是java.util.GregorianCalendar

        //2.常用方法
        //get()
        int days = calendar.get(Calendar.DAY_OF_MONTH);
        System.out.println(days);   //10
        System.out.println(calendar.get(Calendar.DAY_OF_YEAR)); //131,今天是这一年的131天

        //set()
        //calendar可变性
        calendar.set(Calendar.DAY_OF_MONTH,22);
        days = calendar.get(Calendar.DAY_OF_MONTH);
        System.out.println(days);   //22

        //add()
        calendar.add(Calendar.DAY_OF_MONTH,-3);
        days = calendar.get(Calendar.DAY_OF_MONTH);
        System.out.println(days);   //22-3 --》19

        //getTime():日历类---> Date
        Date date = calendar.getTime();
        System.out.println(date);   //Tue May 19 17:12:06 CST 2020

        //setTime():Date ---> 日历类
        Date date1 = new Date();
        calendar.setTime(date1);
        days = calendar.get(Calendar.DAY_OF_MONTH);
        System.out.println(days);   //10
    }
}

3、JDK 8中新日期时间API

LocalDate、LocalTime、LocalDateTime、Instant、DateTimeFormatter、其它类

3.1 新日期时间API出现的背景

(1)如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间
与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经
在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:
可变性:像日期和时间这样的类应该是不可变的
偏移性:Date中的年份是从1900开始的,而月份都从0开始
格式化:格式化只对Date有用,Calendar则不行
此外,它们也不是线程安全的;不能处理闰秒等

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

​ Java 8 吸收了Joda-Time 的精华,以一个新的开始为Java 创建优秀的API。新的java.time 中包含了所有关于
​ 本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和
​ 持续时间(Duration)的类
​ 历史悠久的Date 类新增了toInstant()方法,用于把Date 转换成新的表示形式
​ 这些新增的本地化时间日期API大大简化了日期时间和本地化的管理

(3)java.time–包含值对象的基础包
java.time.chrono–提供对不同的日历系统的访问
java.time.format–格式化和解析时间和日期
java.time.temporal–包括底层框架和扩展特性
java.time.zone–包含时区支持的类
说明:大多数开发者只会用到基础包和format包,也可能会用到temporal包
因此,尽管有68个新的公开类型,大多数开发者,大概将只会用到其中的三分之一

3.2 LocalDate、LocalTime、LocalDateTime的使用

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

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

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

import org.junit.Test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class JDK8DateTimeTest {
    public void test1(){
        //now():获取当前的日期、时间、日期+时间
        LocalDate localDate = LocalDate.now();  //2022-02-19
        LocalTime localTime = LocalTime.now();  //14:41:32.659 到毫秒
        LocalDateTime localDateTime = LocalDateTime.now(); //2022-02-19T14:41:32.659

        System.out.println(localDate);
        System.out.println(localTime);
        System.out.println(localDateTime);

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

        //getXxx():获取相关的属性
        System.out.println(localDateTime.getDayOfMonth());
        System.out.println(localDateTime.getDayOfWeek());
        System.out.println(localDateTime.getMonth());
        System.out.println(localDateTime.getMonthValue());
        System.out.println(localDateTime.getMinute());

        //体现不可变性
        //withXxx():设置相关的属性
        LocalDate localDate1 = localDate.withDayOfMonth(22);
        System.out.println(localDate); //2022-02-19  当前时间不改变
        System.out.println(localDate1);//2022-02-22  只是设置了新的时间,但是当前的时间不变

        LocalDateTime localDateTime2 = localDateTime.withHour(4);
        System.out.println(localDateTime); //2022-02-19T14:41:32.659 当前时间不变
        System.out.println(localDateTime2);//2022-02-19T04:41:32.659 只是设置了新的时间

        //不可变性
        LocalDateTime localDateTime3 = localDateTime.plusMonths(3); //加了3个月
        System.out.println(localDateTime);
        System.out.println(localDateTime3);

        LocalDateTime localDateTime4 = localDateTime.minusDays(6);  //减了6天
        System.out.println(localDateTime);
        System.out.println(localDateTime4);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LiDxfNBn-1645788496332)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\914ba4708727b970905de339e5817e25.png)]

3.3 Instant类的使用(面向机器用的)

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

(1 ns = 10-9s) 1秒= 1000毫秒=106微秒=109纳秒

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

import java.time.*;
public class JDK8DateTimeTest {
    public void test2(){
        //now():获取本初子午线对应的标准时间
        Instant instant = Instant.now();
        System.out.println(instant);    //2020-05-10T09:55:55.561Z

        //添加时间的偏移量
        OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));//东八区
        System.out.println(offsetDateTime); //2020-05-10T18:00:00.641+08:00

        //toEpochMilli():获取自1970年1月1日0时0分0秒(UTC)开始的毫秒数  ---> Date类的getTime()
        long milli = instant.toEpochMilli();
        System.out.println(milli);  //1589104867591

        //ofEpochMilli():通过给定的毫秒数,获取Instant实例  -->Date(long millis)
        Instant instant1 = Instant.ofEpochMilli(1550475314878L);
        System.out.println(instant1);   //2019-02-18T07:35:14.878Z
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vDTT3Tur-1645788496333)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\ec6afaca3ba677f17ecb872ea9ce28f3.png)]

3.4 DateTimeFormatter的使用(类似于SimpleDateFormat)

java.time.format.DateTimeFormatter 类,该类提供了三种格式化方法:
①预定义的标准格式 如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
②本地化相关的格式 如:ofLocalizedDateTime(FormatStyle.LONG)
③自定义的格式 如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

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

public class JDK8DateTimeTest {
    public void test3(){
        //方式一:预定义的标准格式   如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
        DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        //格式化:日期-->字符串
        LocalDateTime localDateTime = LocalDateTime.now();
        String str1 = formatter.format(localDateTime);
        System.out.println(localDateTime);
        System.out.println(str1);//2020-05-10T18:26:40.234

        //解析:字符串 -->日期
        TemporalAccessor parse = formatter.parse("2020-05-10T18:26:40.234");
        System.out.println(parse);

        //方式二:
        //本地化相关的格式 如:ofLocalizedDateTime()
        //FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT:适用于LocalDateTime
    DateTimeFormatter formatter1=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
        //格式化
        String str2 = formatter1.format(localDateTime);
        System.out.println(str2);//2020年5月10日 下午06时26分40秒

        //本地化相关的格式 如:ofLocalizedDate()
  //FormatStyle.FULL/FormatStyle.LONG/FormatStyle.MEDIUM/FormatStyle.SHORT:适用于LocalDate
    DateTimeFormatter formatter2=DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
        //格式化
        String str3 = formatter2.format(LocalDate.now());
        System.out.println(str3);//2020-5-10

       //重点: 方式三:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
       DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        //格式化
        String str4 = formatter3.format(LocalDateTime.now());
        System.out.println(str4);//2020-05-10 06:26:40

        //解析
        TemporalAccessor accessor = formatter3.parse("2020-05-10 06:26:40");
        System.out.println(accessor);
    }
}

3.5 其它日期时间相关API的使用

ZoneId:该类中包含了所有的时区信息,一个时区的ID,如Europe/Paris
ZonedDateTime:一个在ISO-8601日历系统时区的日期时间,如2007-12-03T10:15:30+01:00Europe/Paris
其中每个时区都对应着ID,地区ID都为“{区域}/{城市}”的格式,例如:Asia/Shanghai等
Clock:使用时区提供对当前即时、日期和时间的访问的时钟
持续时间:Duration,用于计算两个“时间”间隔
日期间隔:Period,用于计算两个“日期”间隔
TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下一个工作日”等操作
TemporalAdjusters : 该类通过静态方法(firstDayOfXxx()/lastDayOfXxx()/nextXxx())提供了大量的常用
TemporalAdjuster 的实现

import java.time.*;
import java.util.Set;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;

public class JDK8DateTimeTest {
	public void test1(){
		//ZoneId:类中包含了所有的时区信息
		// ZoneId的getAvailableZoneIds():获取所有的ZoneId
		Set<String> zoneIds= ZoneId.getAvailableZoneIds();
		for(String s: zoneIds) {
			System.out.println(s);
		}
		// ZoneId的of():获取指定时区的时间
		LocalDateTime localDateTime= LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
		System.out.println(localDateTime);
		
		//ZonedDateTime:带时区的日期时间
		// ZonedDateTime的now():获取本时区的ZonedDateTime对象
		ZonedDateTime zonedDateTime= ZonedDateTime.now();
		System.out.println(zonedDateTime);
		// ZonedDateTime的now(ZoneId id):获取指定时区的ZonedDateTime对象
		ZonedDateTime zonedDateTime1= ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
		System.out.println(zonedDateTime1);
	}
    public void test2(){
		//Duration:用于计算两个“时间”间隔,以秒和纳秒为基准
		LocalTime localTime= LocalTime.now();
		LocalTime localTime1= LocalTime.of(15, 23, 32);
		//between():静态方法,返回Duration对象,表示两个时间的间隔
		Duration duration= Duration.between(localTime1, localTime);
		System.out.println(duration);
		
		System.out.println(duration.getSeconds());
		System.out.println(duration.getNano());
		
		LocalDateTime localDateTime= LocalDateTime.of(2016, 6, 12, 15, 23, 32);
		LocalDateTime localDateTime1= LocalDateTime.of(2017, 6, 12, 15, 23, 32);
		
		Duration duration1= Duration.between(localDateTime1, localDateTime);
		System.out.println(duration1.toDays());
	}
    public void test3(){
		//Period:用于计算两个“日期”间隔,以年、月、日衡量
		LocalDate localDate= LocalDate.now();
		LocalDate localDate1= LocalDate.of(2028, 3, 18);
		
		Period period= Period.between(localDate, localDate1);
		System.out.println(period);
        System.out.println(period.getYears());
		
		System.out.println(period.getMonths());
		System.out.println(period.getDays());
		
		Period period1= period.withYears(2);
		System.out.println(period1);
	}
}

​ 与传统日期处理的转换:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Y4Orkeh-1645788496334)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\316c319a548bac159483c82432690094.png)]

4、Java比较器

Comparable接口、Comparator接口

4.1 概述

​ Java中的对象,正常情况下,只能进行比较:==或 != ,不能使用 >或<,但是在开发场景中,我们需要对
​ 多个对象进行排序,言外之意,就需要比较对象的大小
​ 如何实现?使用两个接口中的任何一个:Comparable或 Comparator

​ Java实现对象排序的方式有两种:
​ 自然排序:java.lang.Comparable(重写的方法再sort调用,用sort规定好了只能从小到大排)
​ 定制排序:java.util.Comparator(自己任意定哪种)

4.2 Comparable

(1)Comparable接口的使用举例:自然排序(String等常用的已有类)
①像String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较两个对象大小的方式
②像String、包装类重写compareTo()方法以后,进行了从小到大的排列
③重写compareTo(obj)的规则:
如果当前对象this大于形参对象obj,则返回正整数
如果当前对象this小于形参对象obj,则返回负整数
如果当前对象this等于形参对象obj,则返回零

   public void test1(){
        String[] arr = new String[]{"AA","CC","KK","MM","GG","JJ","DD"}; 
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr))
   }

(2)自定义类实现Comparable自然排序(自定义类实现接口,再重写实现compareTo(obj)方法)

//对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compareTo(obj)方法,
//在compareTo(obj)方法中指明如何排序,就会在Array.sort()里使用这个自定义的排序方法

import org.junit.Test;
import java.util.Arrays;
public class CompareTest {
    public void test2(){
        Goods[] arr = new Goods[5];
        arr[0] = new Goods("lenovoMouse",34);
        arr[1] = new Goods("dellMouse",43);
        arr[2] = new Goods("xiaomiMouse",12);
        arr[3] = new Goods("huaweiMouse",65);
        arr[4] = new Goods("microsoftMouse",43);
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
//商品类
public class Goods implements Comparable{
    private String name;
    private double price;

    public Goods() {
    }
    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    //指明商品比较大小的方式:按照价格从低到高排序,再按照产品名称从高到低排序
    @Override
    public int compareTo(Object o) {
      //System.out.println("**************");
        if(o instanceof Goods){
            Goods goods = (Goods)o;
            //方式一:
            if(this.price > goods.price){
                return 1;
            }else if(this.price < goods.price){
                return -1;
            }else{
              //return 0;
                return -this.name.compareTo(goods.name);
            }
         //方式二:
         //return Double.compare(this.price,goods.price);
        }
      //return 0;
        throw new RuntimeException("传入的数据类型不一致!");
    }
}

4.3 使用Comparator实现定制排序

Comparator接口的使用:定制排序
背景:当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码
或者实现了java.lang.Comparable 接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator
的对象来排序
重写compare(Object o1,Object o2)方法,比较o1和o2的大小:
如果方法返回正整数,则表示o1大于o2
如果返回0,表示相等
返回负整数,表示o1小于o2

import org.junit.Test;
import java.util.Arrays;
import java.util.Comparator;

public class CompareTest {
    public void test3(){
        String[] arr = new String[]{"AA","CC","KK","MM","GG","JJ","DD"};
        Arrays.sort(arr,new Comparator(){
            //按照字符串从大到小的顺序排列
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof String && o2 instanceof  String){
                    String s1 = (String) o1;
                    String s2 = (String) o2;
                    return -s1.compareTo(s2);
                }
              //return 0;
                throw new RuntimeException("输入的数据类型不一致");
            }
        });
        System.out.println(Arrays.toString(arr));
    }
    @Test
    public void test4(){
        Goods[] arr = new Goods[6];
        arr[0] = new Goods("lenovoMouse",34);
        arr[1] = new Goods("dellMouse",43);
        arr[2] = new Goods("xiaomiMouse",12);
        arr[3] = new Goods("huaweiMouse",65);
        arr[4] = new Goods("huaweiMouse",224);
        arr[5] = new Goods("microsoftMouse",43);

        Arrays.sort(arr, new Comparator() {
            //指明商品比较大小的方式:按照产品名称从低到高排序,再按照价格从高到低排序
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Goods && o2 instanceof Goods){
                    Goods g1 = (Goods)o1;
                    Goods g2 = (Goods)o2;
                    if(g1.getName().equals(g2.getName())){
                        return -Double.compare(g1.getPrice(),g2.getPrice());
                    }else{
                        return g1.getName().compareTo(g2.getName());
                    }
                }
                throw new RuntimeException("输入的数据类型不一致");
            }
        });
        System.out.println(Arrays.toString(arr));
    }

4.4 Comparable接口与Comparator的使用的对比

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

5、System类、Math类、BigInteger与BigDecimal

5.1 System类

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

(2)成员变量
System类内部包含in、out和err三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和
标准错误输出流(显示器)
(3)成员方法
①native long currentTimeMillis()
该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间
(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数
②void exit(int status)
该方法的作用是退出程序,其中status的值为0代表正常退出,非零代表异常退出
使用该方法可以在图形界面编程中实现程序的退出功能等
③void gc()
该方法的作用是请求系统进行垃圾回收,至于系统是否立刻回收,则取决于系统中垃圾回收算法
的实现以及系统执行时的情况
④getProperty(String key)
该方法的作用是获得系统中属性名为key的属性对应的值
系统中常见的属性名以及属性的作用如下表所示: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6MEMlP95-1645788496334)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\a487be6770c3eacff3bc0bb872ab1d27.png)]

5.2 Math类

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

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

5.3 BigInteger与BigDecimal

BigInteger

(1)Integer类作为int的包装类,能存储的最大整型值为2^31 -1,Long类也是有限的,最大为2^63 -1
如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了

​ java.math包的BigInteger可以表示不可变的任意精度的整数
​ BigInteger提供所有Java 的基本整数操作符的对应物,并提供java.lang.Math 的所有相关方法
​ 另外,BigInteger还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作
(2)构造器
​ BigInteger(String val):根据字符串构建BigInteger对象
(3)常用方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MfmHUIhh-1645788496335)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\a1b9e65f934b5efe345447284641b6b5.png)]

BigDecimal

(1)一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,
故用到java.math.BigDecimal类
BigDecimal类支持不可变的、任意精度的有符号十进制定点数
(2)构造器
public BigDecimal(double val)
public BigDecimal(String val)
(3)常用方法
public BigDecimal add(BigDecimal augend)
public BigDecimal subtract(BigDecimal subtrahend)
public BigDecimal multiply(BigDecimal multiplicand)
public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

public void test2() {
        BigInteger bi = new BigInteger("1243324112234324324325235245346567657653");
        BigDecimal bd = new BigDecimal("12435.351");
        BigDecimal bd2 = new BigDecimal("11");
        System.out.println(bi);
      //System.out.println(bd.divide(bd2));
        System.out.println(bd.divide(bd2, BigDecimal.ROUND_HALF_UP));
        System.out.println(bd.divide(bd2, 25, BigDecimal.ROUND_HALF_UP));
}

三、枚举与注解

1、枚举类的使用(可以有限列举出来对象的类)

1.1 枚举类的理解

枚举类:类的对象只有有限个,确定的,我们称此类为枚举类
举例如下:星期:Monday(星期一)、…、Sunday(星期天)
性别:Man(男)、Woman(女)
季节:Spring(春节)…Winter(冬天)
支付方式:Cash、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
就职状态:Busy、Free、Vocation、Dimission
订单状态:Nonpayment(未付款)、Paid(已付款)、Delivered(已发货)、Return(退货)、
Checked(已确认)Fulfilled(已配货)
线程状态:创建、就绪、运行、阻塞、死亡
当需要定义一组常量时,强烈建议使用枚举类
枚举类的实现:JDK1.5之前需要自定义枚举类
JDK 1.5 新增的enum 关键字用于定义枚举类
若枚举只有一个对象, 则可以作为一种单例模式的实现方式

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

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

public class SeasonTest {
    public static void main(String[] args) {
        Season spring = Season.SPRING;
        System.out.println(spring);  //输出对象,即输出对象中的toString()方法
    }
}
//自定义枚举类(属性和构造器是private的,其他都public)
class Season{
    //1.声明Season对象的属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;

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

    //3.提供当前枚举类的多个对象
    public static final Season SPRING = new Season("春天","万物复苏");
    public static final Season SUMMER = new Season("夏天","烈日炎炎");
    public static final Season AUTUMN = new Season("秋天","金秋送爽");
    public static final Season WINTER = new Season("冬天","白雪皑皑");

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

    public String getSeasonDesc() {
        return seasonDesc;
    }

    //4.其他诉求1:提供toString()
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}

1.3、使用enum关键字定义枚举类(JDK 1.5新增enum用于定义枚举类)

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

public class SeasonTest1 {
    public static void main(String[] args) {
        Season1 summer = Season1.SUMMER;
      //toString():
        System.out.println(summer.toString());   
        System.out.println(Season1.class.getSuperclass());
    }
}

//使用enum关键字枚举类
enum Season1{
    //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
    SPRING("春天","万物复苏"),
    SUMMER("夏天","烈日炎炎"),
    AUTUMN("秋天","金秋送爽"),
    WINTER("冬天","白雪皑皑");

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

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

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

    public String getSeasonDesc() {
        return seasonDesc;
    }

    //4.其他诉求1:提供toString()
//    @Override
//    public String toString() {
//        return "Season{" +
//                "seasonName='" + seasonName + '\'' +
//                ", seasonDesc='" + seasonDesc + '\'' +
//                '}';
//    }
}

1.4 Enum类中的常用方法

主要方法:
values():返回枚举类型的对象数组,该方法可以很方便地遍历所有的枚举值
valueOf(String str):可以把一个字符串转为对应的枚举类对象,要求字符串必须是枚举类对象的“名字”
如不是,会有运行时异常:IllegalArgumentException。
toString():返回当前枚举类对象常量的名称 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Brg6sndS-1645788496336)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\c8159e5419e4c0375de1312dc227d2c0.png)]

public class SeasonTest1 {
    public static void main(String[] args) {
        Season1 summer = Season1.SUMMER;
      //toString():
        System.out.println(summer.toString());

      //System.out.println(Season1.class.getSuperclass());
        System.out.println("**************************");
      //values():返回所有的枚举类对象构成的数组
        Season1[] values = Season1.values();
        for(int i = 0;i < values.length;i++){
            System.out.println(values[i]);
        }
        System.out.println("****************************");
        Thread.State[] values1 = Thread.State.values();
        for(int i = 0;i < values1.length;i++){
            System.out.println(values1[i]);
        }

      //valueOf(String objName):返回枚举类中对象名是objName的对象。
        Season1 winter = Season1.valueOf("WINTER");
      //如果没有objName的枚举类对象,则抛异常:IllegalArgumentException
      //Season1 winter = Season1.valueOf("WINTER1");
        System.out.println(winter);
    }
}
//输出
SUMMER
class java.lang.Enum
**************************
SPRING
SUMMER
AUTUMN
WINTER
****************************
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
WINTER

1.5、使用enum关键字定义的枚举类实现接口

使用enum关键字定义的枚举类实现接口的情况
情况一:实现接口,在enum类中实现抽象方法
情况二:让枚举类的对象分别实现接口中的抽象方法

public class SeasonTest1 {
    public static void main(String[] args) {
        //values():返回所有的枚举类对象构成的数组
        Season1[] values = Season1.values();
        for(int i = 0;i < values.length;i++){
            System.out.println(values[i]);
            values[i].show();
        }

        //valueOf(String objName):返回枚举类中对象名是objName的对象。
        Season1 winter = Season1.valueOf("WINTER");
        winter.show();
    }
}

interface Info{
    void show();
}

//使用enum关键字枚举类
enum Season1 implements Info{
    //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
    SPRING("春天","春暖花开"){
        @Override
        public void show() {
            System.out.println("一元复始、万物复苏");
        }
    },
    SUMMER("夏天","夏日炎炎"){
        @Override
        public void show() {
            System.out.println("蝉声阵阵、烈日当空");
        }
    },
    AUTUMN("秋天","秋高气爽"){
        @Override
        public void show() {
            System.out.println("天高气清、金桂飘香");
        }
    },
    WINTER("冬天","冰天雪地"){
        @Override
        public void show() {
            System.out.println("寒冬腊月、滴水成冰");
        }
    };

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

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

    //4.其他诉求:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
    //4.其他诉求1:提供toString()
//    @Override
//    public String toString() {
//        return "Season{" +
//                "seasonName='" + seasonName + '\'' +
//                ", seasonDesc='" + seasonDesc + '\'' +
//                '}';
//    }

//    @Override
//    public void show() {
//        System.out.println("这是一个季节。");
//    }
}

2、注解Annotation的使用

2.1 注解的理解

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

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

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

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

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

2.2 Annotation的使用示例

使用Annotation 时要在其前面增加@ 符号, 并把该Annotation 当成一个修饰符使用,用于修饰它支持的程序元素
示例一:生成文档相关的注解
@author标明开发该类模块的作者,多个作者之间使用,分割
@version标明该类模块的版本
@see参考转向,也就是相关主题
@since从哪个版本开始增加的
@param对方法中某参数的说明,如果没有参数就不能写
@return对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception对方法可能抛出的异常进行说明,如果方法没有用throws显式抛出的异常就不能写其中
@param@return和@exception这三个标记都是只用于方法的。
@param的格式要求:@param形参名形参类型形参说明
@return的格式要求:@return返回值类型返回值说明
@exception的格式要求:@exception异常类型异常说明
@param和@exception可以并列多个
示例二:在编译时进行格式检查(JDK内置的三个基本注解)
@Override: 限定重写父类方法, 该注解只能用于方法(若标志了这个,则编译器会检查是否重写了)
@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时,通常是因为所修饰的结构危险或存在更好选择
@SuppressWarnings: 抑制编译器警告 (@SuppressWarnings(“unused”))
示例三:跟踪代码依赖性,实现替代配置文件功能
Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署
spring框架中关于“事务”的管理
import java.util.ArrayList;
import java.util.Date;
以前需要这么写,在web.xml文件中进行Servlet的部署,使得知道在进入/checkUser页面时
用com.xurui.servlet里的CheckUserServlet类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kk2MdcDB-1645788496336)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\image-20220220195107230.png)]

​ 现在只需要在com.xurui.servlet里的CheckUserServlet类前面一行加上@WebServlet("/checkUser")来告知

2.3 如何自定义注解

​ 定义新的Annotation类型使用**@interface关键字
​ 自定义注解自动继承了
java.lang.annotation.Annotation**接口

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

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

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

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

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

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

public @interface MyAnnotation {
    String value(); //方法
}
/*如何自定义注解:参照@SuppressWarnings定义
  ① 注解声明为:@interface
  ② 内部定义成员,通常使用value表示
  ③ 可以指定成员的默认值,使用default定义
  ④ 如果自定义注解没有成员,表明是一个标识作用
    
  如果注解有成员,在使用注解时,需要指明成员的值。
  自定义注解必须配上注解的信息处理流程(使用反射)才有意义。
  自定义注解通过都会指明两个元注解:Retention、Target
*/

@MyAnnotation(value = "hello")   //@MyAnnotation()会报错,因为无参数

2.4 jdk中4个基本的元注解的使用1(Retention)

​ JDK 的元Annotation用于修饰其他Annotation定义(元注解:对现有注解进行解释说明)

​ JDK5.0提供了4个标准的meta-annotation类型,分别是:Retention
​ Target
​ Documented
​ Inherited
​ 元数据的理解:String name = “MyBlog”;(将String和name看作元数据,对"MyBlog"进行修饰)

Retention:指定所修饰的 Annotation 的生命周期:SOURCE\CLASS(默认行为)\RUNTIME只有声明
​ 为RUNTIME生命周期的注解,才能通过反射获取

​ @Retention: 只能用于修饰一个Annotation定义, 用于指定该Annotation 的生命周期, @Rentention包含一个
​ RetentionPolicy类型的成员变量, 使用@Rentention时必须为该value成员变量指定值:
​ RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释
​ RetentionPolicy.CLASS:在class文件中有效(即class保留),当运行Java 程序时, JVM 不会保留注解,
​ 这是默认值
​ RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行Java 程序时, JVM 会保留注释,
​ 程序可以通过反射获取该注释
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6SIZ9ojT-1645788496337)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\655f60ccb2126424b1b8f7f2cf4c4cde.png)]

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
    String value();
}

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

@MyAnnotation(value = "hello")
class Person{
    private String name;
    private int age;

    public Person() {
        super();
    }
    
    @MyAnnotation(value = "jack")
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void walk(){
        System.out.println("学习中……");
    }
    public void eat(){
        System.out.println("摸鱼中……");
    }
}

2.5 jdk中4个基本的元注解的使用2

​ @Target: 用于修饰Annotation 定义, 用于指定被修饰的Annotation 能用于修饰哪些程序元素
​ @Target 也包含一个名为value 的成员变量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CS5FJ8zN-1645788496338)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\55f870273e1957635488dbcab66a299d.png)]

​ @Documented: 用于指定被该元Annotation 修饰的Annotation类将被javadoc工具提取成文档
​ 默认情况下,javadoc是不包括注解的
​ 定义为Documented的注解必须设置Retention值为RUNTIME
​ 表示所修饰的注解在被javadoc解析时,保留下来

​ @Inherited: 被它修饰的Annotation 将具有继承性
​ 如果某个类使用了被@Inherited 修饰的Annotation, 则其子类将自动具有该注解
​ 比如:如果把标有@Inherited注解的自定义的注解标注在类级别上,子类则可以继承父类类级别的注解
​ 实际应用中,使用较少

​ 常用@Retention指明生命周期,@Target指明可用结构,另外两个不常用

import org.junit.Test;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Date;

public class AnnotationTest {
   public static void main(String[] args) { }
    
   @Test
   public void testGetAnnotation(){
       Class clazz = Student.class;
       Annotation[] annotations = clazz.getAnnotations();
       for(int i = 0;i < annotations.length;i++){
           System.out.println(annotations[i]);
       }
   }
}

@MyAnnotation(value = "hello")
class Person{
    private String name;
    private int age;
    public Person() {
        super();
    }    
    @MyAnnotation
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }   
    @MyAnnotation
    public void walk(){
        System.out.println("学习中……");
    }
    public void eat(){
        System.out.println("摸鱼中……");
    }
}
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE,TYPE_PARAMETER,TYPE_USE})
public @interface MyAnnotation {
    String value() default "book";
}

2.6 利用反射获取注解信息

​ JDK 5.0 在java.lang.reflect包下新增了AnnotatedElement接口, 该接口代表程序中可以接受注解的程序元素

​ 当一个Annotation 类型被定义为运行时Annotation后, 该注解才是运行时可见, 当class文件被载入时保存在
​ class 文件中的Annotation 才会被虚拟机读取

​ 程序可以调用AnnotatedElement对象的如下方法来访问Annotation 信息

2.7 jdk8新特性:可重复注解

​ Java 8对注解处理提供了两点改进:可重复的注解及可用于类型的注解
​ 此外,反射也得到了加强,在Java8中能够得到方法参数的名称,这会简化标注在方法参数上的注解

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
public @interface MyAnnotations {
    MyAnnotation[] value();
}


import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR,
 LOCAL_VARIABLE,TYPE_PARAMETER,TYPE_USE})
public @interface MyAnnotation {
    String value() default "hello";
}


import java.lang.annotation.Annotation;

/*jdk 8 中注解的新特性:可重复注解、类型注解
  可重复注解:① 在MyAnnotation上声明@Repeatable,成员值为MyAnnotations.class
            ② MyAnnotation的Target和Retention等元注解与MyAnnotations相同。
*/
public class AnnotationTest {
   public static void main(String[] args) { }
}

@MyAnnotation(value = "hi")
@MyAnnotation(value = "abc")
//jdk 8之前的写法:  创建个数组
//@MyAnnotations({@MyAnnotation(value="hi"),@MyAnnotation(value="hi")})


2.8 jdk8新特性:类型注解

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

​ 在Java8之前,注解只能是在声明的地方所使用,Java8开始,注解可以应用在任何地方
​ ElementType.TYPE_PARAMETER表示该注解能写在类型变量的声明语句中(如:泛型声明)
​ ElementType.TYPE_USE表示该注解能写在使用类型的任何语句中。
​ import java.util.ArrayList;

/*jdk 8 中注解的新特性:可重复注解、类型注解
    可重复注解:① 在MyAnnotation上声明@Repeatable,成员值为MyAnnotations.class
              ② MyAnnotation的Target和Retention等元注解与MyAnnotations相同  
    类型注解:ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)
            ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中。
*/
import java.util.ArrayList;
public class AnnotationTest {

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

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE,TYPE_PARAMETER,TYPE_USE})
public @interface MyAnnotation {
    String value() default "hello";
}

四、集合(和数组一样)

HashMap、ConcurrentHashMap、各种List,最好结合源码看

就说最简单和普遍的HashMap,让你讲讲,你就可以先说说hashMap的设计原理,底层结构(链表+数组)扩容方式等,从这你就可以说说这种设计好在哪里(比如讲一讲put是如何做hash的),这时候你可以说这种hash可能会有冲突,hashMap也是做了相应设计的。

然后面试官会问题你怎么解决冲突?你可以再给他讲讲解决hash冲突的三种通常方式,而hashMap用的是链式法,然后可以说到这样会有隐患就是hash链过长。

面试官再问,你会给他讲解决复杂度高的长链用了红黑树的结构,这里还可以延伸到红黑树的特点或者jdk7和jdk8的不同实现,这时候你可以说解决hash冲突,但hashMap还会有并发和同步的问题。

面试官会让你再讲讲,你可以说说hashtable是线程安全的,怎么实现的(sync函数),并不好,从而引出更好的juc包,说说concurrentHashMap,之后又可以说道锁分段原理,弱一致性迭代器,concurrentHashMap的锁粒度(java7和java8不同),同包的CopyOnWriteArray等等。

你还可以延伸说到锁(重量、轻量、悲观乐观各自实现、底层源码等等)、缓存(因为很多时候Map的结构可以作为缓存,从而可以说到缓存系统的设计,kv原理,分布式缓存redis、memcashed等等)

1、Java 集合框架概述

1.1 集合框架与数组的对比及概述

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

​ 数组在存储多个数据封面的特点:
​ 一旦初始化以后,它的长度就确定了,不可修改
​ 数组一旦定义好,它的数据类型也就确定了,我们就只能操作指定类型的数据了
​ 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高
​ 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
​ 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJyEogMu-1645788496338)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\7dd14b25935d46725bf3e42f9c47a47a.png)]

1.2 集合框架涉及到的API

(1)Java 集合可分为Collection和Map两种体系
Collection接口:单列数据,定义了存取一组对象的方法的集合
List接口:元素有序、可重复的集合
Set接口:元素无序、不可重复的集合
Map接口:双列数据,保存具有映射关系“key-value对”的集合(Map即映射)

(2)Collection接口:单列集合,用来存储一个一个的对象
List接口:存储有序的、可重复的数据 【“动态”数组】
ArrayList、LinkedList、Vector
Set接口:存储无序的、不可重复的数据 【高中讲的“集合”】
HashSet、LinkedHashSet、TreeSet
Map接口:双列集合,用来存储一对(key-value)一对的数据 【高中函数:y = f(x)】
HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XwyTKghn-1645788496339)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\4a9d3171541866ba77e2291ce41e9962.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Avq4D3XY-1645788496340)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\a1fdb6d75598e87a224697d12c2c2bcf.png)]

​ 实线是继承关系(接口),虚线是实现关系(实现类)

2、Collection接口方法

2.1 Collection接口

​ Collection 接口是List、Set 和Queue 接口的父接口,该接口里定义的方法既可用于操作Set 集合,也可用于
​ 操作List 和Queue 集合

​ JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现

​ 在Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成Object 类型处理;
​ 从JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型

所有方法中的obj对象,用对象的内容判断,而不是用对象的内存地址判断,故必须重写equals方法

2.2 Collection接口中的常用方法1

(1)添加
add(Objec tobj) 参数只能填对象,所以填一些基本数据类型的话会自动装箱
addAll(Collection coll)
(2)获取有效元素的个数
int size()
(3)清空集合
void clear()
(4)是否是空集合
boolean isEmpty()
(5)是否包含某个元素
boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
boolean containsAll(Collection c):也是调用元素的equals方法来比较的,拿两个集合的元素挨个比较
(6)删除
boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素
只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
(7)取两个集合的交集
boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
(8)集合是否相等
boolean equals(Object obj)
(9)转成对象数组
Object[] toArray()
(10)获取集合对象的哈希值
hashCode()
(11)遍历
iterator():返回迭代器对象,用于集合遍历
向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()(不重写会判断地址相等)

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;

public class CollectionTest {
    @Test
    public void test1(){
        Collection coll = new ArrayList();
        
        //add(Object e):将元素e添加到集合coll中
        coll.add("AA");
        coll.add("BB");
        coll.add(123);  //自动装箱
        coll.add(new Date());

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

        //addAll(Collection coll1):将coll1集合中的元素添加到当前的集合中
        Collection coll1 = new ArrayList();
        coll1.add(456);
        coll1.add("CC");
        coll.addAll(coll1);

        System.out.println(coll.size());    //6
        System.out.println(coll);

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

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


import java.util.Objects;
public class Person {
    private String name;
    private int age;

    public Person() {
        super();
    }
    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 +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        System.out.println("Person equals()....");
        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);
    }
}

//向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals().
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

public class CollectinoTest {
    @Test
    public void test(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        //Person p = new Person("Jerry",20);
        //coll.add(p);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false); //boolean型自动装箱

        //1.contains(Object obj):判断当前集合中是否包含obj(对象)
        //我们在调用contain判断时会调用obj对象所在类的equals()。
        boolean contains = coll.contains(123);
        System.out.println(contains);
        System.out.println(coll.contains(new String("Tom"))); //String重写过equals方法,故比 
        //System.out.println(coll.contains(p));//true            较内容,而不是内存地址       
        System.out.println(coll.contains(new Person("Jerry",20)));//false -->true(重写)

        //2.containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合中。
        Collection coll1 = Arrays.asList(123,4567);
        System.out.println(coll.containsAll(coll1));
        
        //3.remove(Object obj):从当前集合中移除obj元素
        coll.remove(1234);
        System.out.println(coll);

        coll.remove(new Person("Jerry",20));
        System.out.println(coll);

        //4. removeAll(Collection coll1):差集:从当前集合中移除coll1中所有的元素。
        Collection coll1 = Arrays.asList(123,456);
        coll.removeAll(coll1);
        System.out.println(coll);
        
        //5.void retainAll(Collection coll1) 交集:获取当前集合和coll1集合的交集,并返回给当前集合
        //Collection coll1 = Arrays.asList(123,456,789);
        //coll.retainAll(coll1);
        //System.out.println(coll);

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

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

        //8.集合--->数组:toArray()
        Object[] arr = coll.toArray();
        for(int i = 0;i < arr.length;i++){
            System.out.println(arr[i]);
        }

        //拓展:数组--->集合:调用Arrays类的静态方法asList()
        List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
        System.out.println(list);

        List arr1 = Arrays.asList(123, 456);
        System.out.println(arr1);//[123, 456]

        List arr2 = Arrays.asList(new int[]{123, 456}); //这样写会认为是一个元素
        System.out.println(arr2.size());//1

        List arr3 = Arrays.asList(new Integer[]{123, 456});  //类的对象才是2个
        System.out.println(arr3.size());//2

        //9.iterator():返回Iterator接口的实例,用于遍历集合元素。放在IteratorTest.java中测试
    }
}

3、Iterator迭代器接口(仅可遍历,不是容器装了)

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

GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的
内部细节
迭代器模式,就是为容器而生
类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmyYoDKd-1645788496340)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\92824284e855a3c578bcd9a977bc528c.png)]

3.1 使用Iterator遍历Collection和Iterator遍历集合的两种错误写法

//集合元素的遍历操作,使用迭代器Iterator接口
//内部的方法:hasNext()和 next()
//集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class IteratorTest {
    @Test
    public void test(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);

        Iterator iterator = coll.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());
        //报异常:NoSuchElementException
        //因为:在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,
        //     直接调用it.next()会抛出NoSuchElementException异常。
        //System.out.println(iterator.next());

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

        //方式三:推荐
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
        
        //两种错误写法
        /*
        错误方式一:
        Iterator iterator = coll.iterator();
        while(iterator.next() != null){     集合中有可能是空的是错的
            System.out.println(iterator.next());
        }
        */
        
        /*
        错误方式二:
        集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
        */
        while(coll.iterator().hasNext()){
            System.out.println(coll.iterator().next()); //不断输出第一个元素
        }
    }
}

3.2 迭代器Iterator的执行原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z3UB8tCp-1645788496341)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\e22bd60a120c0355cbb2ae3601442cb7.png)]

3.3 Iterator迭代器remove()的使用

​ 迭代器内部定义了remove(),可以在遍历的时候,删除集合中的元素
​ 此方法不同于集合直接调用remove()

​ Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法

​ 如果还未调用next()或在上一次调用next方法之后已经调用了remove方法,再调用remove都会报
​ IllegalStateException

 public void test3(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);

        //删除集合中”Tom”
        //如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,
        //再调用remove都会报IllegalStateException。
        Iterator iterator = coll.iterator();
        while(iterator.hasNext()){
            //iterator.remove();
            Object obj = iterator.next();
            if("Tom".equals(obj)){
                iterator.remove();
                //iterator.remove();                
            }
        }
        //遍历集合
        iterator = coll.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
}

3.4 新特性foreach循环遍历集合或数组(底层调用Iterator完成操作)

​ Java 5.0 提供了foreach循环迭代访问Collection和数组
遍历操作不需获取Collection或数组的长度,无需使用索引访问元素
​ 遍历集合的底层调用Iterator完成操作
​ foreach还可以用来遍历数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GyCxCbkf-1645788496342)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\7f424cc6c34bd887f20adac6cd9afa0f.png)]

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;

public class ForTest {
    @Test
    public void test(){
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new Person("Jerry",20));
        coll.add(new String("Tom"));
        coll.add(false);
                             //即i
        //for(集合中元素的类型 局部变量 : 集合对象),内部仍然调用了迭代器。
        for(Object obj : coll){  //集合对象即要遍历的集合/数组名称
            System.out.println(obj); //将coll集合中的元素取一个,赋给obj,输出,再取一个,赋给obj
        }
    }

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

    //练习题
    @Test
    public void test3(){
        String[] arr = new String[]{"SS","KK","RR"};

        /*
        方式一:普通for赋值
        for(int i = 0;i < arr.length;i++){
            arr[i] = "HH";
        }
        */

        //方式二:增强for循环
        for(String s : arr){
            s = "HH";
        }
        
        for(int i = 0;i < arr.length;i++){
            System.out.println(arr[i]);
        }
    }
}

4、Collection子接口之一:List接口(动态数组)

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

4.1 List接口常用实现类的对比

(1)List接口框架
Collection接口:单列集合,用来存储一个一个的对象
List接口:存储有序的、可重复的数据(“动态”数组,替换原有的数组)
ArrayList:作为List接口的主要实现类;
线程不安全的,效率高;
底层使用Object[] elementData存储
LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;
底层使用双向链表存储
Vector:作为List接口的古老实现类;线程安全的,效率低; //一般不用,效率低
底层使用Object[] elementData存储
(2)面试题:比较ArrayList、LinkedList、Vector三者的异同?
同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
不同:见上

4.2 ArrayList(变长对象的数组)的源码分析

(1)ArrayList是List 接口的典型实现类、主要实现类
本质上,ArrayList是对象引用的一个”变长”数组

(2)jdk 7情况下(扩容是在满了时自动进行
ArrayList list = new ArrayList(); //底层创建了长度是10的Object[]数组elementData
list.add(123); //elementData[0] = new Integer(123);
ilst.add(11); //如果此次的添加导致底层elementData数组容量不够,则扩容
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中
结论:反复扩容太累,建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)

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

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

4.3 LinkedList的源码分析

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

​ LinkedList:双向链表(前后),内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素
​ 同时,定义内部类Node,作为LinkedList中保存数据的基本结构(不是循环链表)

1694eedac8f9664aa96cf0a673679e94

      LinkedList list = new LinkedList(); //内部声明了Node类型的first和last属性,默认值为null
      list.add(123);  //将123封装到Node中,创建了Node对象。
  
      其中,Node定义为:体现了LinkedList的双向链表的说法
      private static class Node<E> {
           E item;
           Node<E> next;
           Node<E> prev;
  
           Node(Node<E> prev, E element, Node<E> next) {
               this.item = element;
               this.next = next;     //next变量记录下一个元素的位置
               this.prev = prev;     //prev变量记录前一个元素的位置
           }
      }

4.4 Vector的源码分析

Vector 是一个古老的集合,JDK1.0就有了
大多数操作与ArrayList相同,区别之处在于Vector是线程安全的
在各种list中,最好把ArrayList作为缺省选择
当插入、删除频繁时,使用LinkedList
Vector总是比ArrayList慢,所以尽量避免使用

jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组
在扩容方面,默认扩容为原来的数组长度的2倍

4.5 List接口中的常用方法测试

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

(1)void add(int index, Object ele):在index位置插入ele元素
(2)boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
(3)Object get(int index):获取指定index位置的元素
(4)int indexOf(Object obj):返回obj在集合中首次出现的位置
(5)int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
(6)Object remove(int index):移除指定index位置的元素,并返回此元素
(7)Object set(int index, Object ele):设置指定index位置的元素为ele
(8)List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合

总结:常用方法
增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:① Iterator迭代器方式
② 增强for循环(foreach)
③ 普通的循环

import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

public class ListTest {
    @Test
    public void test3(){
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");

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

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

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

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

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

    @Test
    public void tets2(){
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom",12));
        list.add(456);
        //int indexOf(Object obj):返回obj在集合中首次出现的位置。如果不存在,返回-1.
        int index = list.indexOf(4567);
        System.out.println(index);

        //int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置。如果不存在,返回-1.
        System.out.println(list.lastIndexOf(456));

        //Object remove(int index):移除指定index位置的元素,并返回此元素
        Object obj = list.remove(0);
        System.out.println(obj);
        System.out.println(list);

        //Object set(int index, Object ele):设置指定index位置的元素为ele
        list.set(1,"CC");
        System.out.println(list);

  //List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的左闭右开区间的子集合
        List subList = list.subList(2, 4);
        System.out.println(subList);
        System.out.println(list);
    }

    @Test
    public void test(){
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom",12));
        list.add(456);

        System.out.println(list);

        //void add(int index, Object ele):在index位置插入ele元素
        list.add(1,"BB");
        System.out.println(list);

        //boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
        List list1 = Arrays.asList(1, 2, 3);
        list.addAll(list1);
//        list.add(list1);
        System.out.println(list.size());//9

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

    }
}

4.6 List的一个面试小题

(1)请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?
ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?

​ ArrayList和LinkedList的异同
​ 二者都线程不安全,相对线程安全的Vector,执行效率高
​ 此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构
​ 对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针
​ 对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据

​ ArrayList和Vector的区别
​ Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类,
​ 因此开销就比ArrayList要大,访问要慢
​ 正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制
​ Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍
​ Vector还有一个子类Stack

(2)区分List中remove(int index)和remove(Object obj)
public class ListEver {
@Test
public void testListRemove() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list);//
}
private void updateList(List list) {
//list.remove(2); 有自动装箱和索引,会先检查索引
list.remove(new Integer(2));
}
}

5、Collection子接口之二:Set接口(无序(哈希值)、不重复)

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

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

​ Set 判断两个对象是否相同不是使用==运算符,而是根据equals()方法

5.1 Set接口实现类的对比

​ Collection接口:单列集合,用来存储一个一个的对象
​ Set接口:存储无序的、不可重复的数据 (高中讲的“集合”)
​ HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值;底层用数组存
​ LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
​ 对于频繁的遍历操作,LinkedHashSet效率高于HashSet
​ TreeSet:底层用二叉树存储,放入其中的数据必须是同一个类型的
​ 可以按照添加对象的指定属性,进行排序

5.2 Set的无序性与不可重复性的理解

①无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
②不可重复性:保证添加的元素按照equals()判断时,不能返回true,即:相同的元素只能添加一个
如果新建一个类,类中没有重写equals方法,则会继承Object类中的equals方法,即比较地址,
内容一样地址不等,故hashset没法检测出这个相同的元素,所以要重写类的equals方法,使其
比较对象的内容,而不是地址

如何保证不可重复性?HashSet用哈希值来提高效率,不用再从头到尾一个个比

import org.junit.Test;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class SetTest {
      @Test
   public void test(){
      Set set = new HashSet();
      set.add(123);
      set.add(456);
      set.add("fgd");
      set.add("book");
      set.add(new User("Tom",12));
      set.add(new User("Tom",12)); //
      set.add(129);

      Iterator iterator = set.iterator();
      while(iterator.hasNext()){
          System.out.println(iterator.next());
      }
   }
}
public class User{
    private String name;
    private int age;

    public User() {
    }

    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

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

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

5.3 HashSet中元素的添加过程(HashSet底层:数组+链表的结构)

(1)HashSet是Set 接口的典型实现,大多数时候使用Set 集合时都使用这个实现类
HashSet按Hash算法来存储集合中的元素,因此具有很好的存取、查找、删除性能

​ HashSet具有以下特点:不能保证元素的排列顺序
​ HashSet不是线程安全的
​ 集合元素可以是null

​ 底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来的2倍
​ HashSet 集合判断两个元素相等的标准:两个对象通过hashCode() 方法比较相等,并且两个对象的equals()
​ 方法返回值也相等

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

(2)添加元素的过程
以HashSet为例:我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的
哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置
(即为:索引位置),判断数组此位置上是否已经有元素:
如果此位置上没有其他元素,则元素a添加成功 ---------->情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a
与元素b的hash值:
如果hash值不相同,则元素a添加成功 ----------->情况2
如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功 ----------->情况3

对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储
jdk 7 :元素a放到数组中,指向原来的元素
jdk 8 :原来的元素在数组中,指向元素a
总结:七上八下

9cadfe3cd3721e9c7ba5f414f57434ec

5.4 关于hashCode()和equals()的重写

5.4.1 重写hashCode() 方法的基本原则

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

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

​ 对象中用作equals() 方法比较的Field,都应该用来计算hashCode值

5.4.2 重写equals() 方法的基本原则

​ 以自定义的Customer类为例,何时需要重写equals()?

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

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

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

5.4.3 Eclipse/IDEA工具里hashCode()的重写

以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode
问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

选择系数的时候要选择尽量大的系数
因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高(减少冲突)

并且31只占用5bits,相乘造成数据溢出的概率较小

31可以由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化(提高算法效率)

31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

要求:向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
要求:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码
重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值

5.5 LinkedHashSet的使用

​ LinkedHashSet是HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和
​ 后一个数据

​ 优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet

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

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

​ LinkedHashSet不允许集合元素重复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v92DISuT-1645788496343)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\142499a55331e1ce03aa23593f2f66a7.png)]

import org.junit.Test;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

public class SetTest {
    @Test
    public void test2(){
        Set set = new LinkedHashSet();
        set.add(456);
        set.add(123);
        set.add(123);
        set.add("AA");
        set.add("CC");
        set.add(new User("Tom",12));
        set.add(new User("Tom",12));
        set.add(129);

        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
public class User{
    private String name;
    private int age;
    public User() {
    }
    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }
    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}

5.6 TreeSet的自然排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MXaiXsds-1645788496344)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\image-20220221215848614.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULRHxiMK-1645788496344)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\image-20220221215924176.png)]

import org.junit.Test;
import java.util.Iterator;
import java.util.TreeSet;
/**
 * 1.向TreeSet中添加的数据,要求是相同类的对象。
 * 2.两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator)
 * 3.自然排序中,比较两个对象是否相同的标准为:compareTo()返回0.不再是equals().
 * 4.定制排序中,比较两个对象是否相同的标准为:compare()返回0.不再是equals().
 */
public class TreeSetTest {
    @Test
    public void test() {
        TreeSet set = new TreeSet();

        //失败:不能添加不同类的对象
//        set.add(123);
//        set.add(456);
//        set.add("AA");
//        set.add(new User("Tom",12));

        //举例一:
//        set.add(34);
//        set.add(-34);
//        set.add(43);
//        set.add(11);
//        set.add(8);

        //举例二:
        set.add(new User("Tom",12));
        set.add(new User("Jerry",32));
        set.add(new User("Jim",2));
        set.add(new User("Mike",65));
        set.add(new User("Jack",33));
        set.add(new User("Jack",56));

        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
public class User implements Comparable{
    private String name;
    private int age;
    public User() {
    }
    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }
    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if (o instanceof User) {
            User user = (User) o;
//            return this.name.compareTo(user.name);  //按照姓名从小到大排列
//            return -this.name.compareTo(user.name);  //按照姓名从大到小排列
            int compare = -this.name.compareTo(user.name);  //按照姓名从大到小排列
            if(compare != 0){   //年龄从小到大排列
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        } else {
            throw new RuntimeException("输入的类型不匹配");
        }
    }
}

5.7 TreeSet的定制排序

​ 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

import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
/**
 * 1.向TreeSet中添加的数据,要求是相同类的对象。
 * 2.两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator)
 * 3.自然排序中,比较两个对象是否相同的标准为:compareTo()返回0.不再是equals().
 * 4.定制排序中,比较两个对象是否相同的标准为:compare()返回0.不再是equals().
 */
public class TreeSetTest {
    @Test
    public void tets2(){
        Comparator com = 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.getAge(),u2.getAge());
                }else{
                    throw new RuntimeException("输入的数据类型不匹配");
                }
            }
        };
        TreeSet set = new TreeSet(com);
        set.add(new User("Tom",12));
        set.add(new User("Jerry",32));
        set.add(new User("Jim",2));
        set.add(new User("Mike",65));
        set.add(new User("Mary",33));
        set.add(new User("Jack",33));
        set.add(new User("Jack",56));

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

public class User implements Comparable{
    private String name;
    private int age;
    public User() {
    }
    public User(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 "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }
    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if (o instanceof User) {
            User user = (User) o;
//            return this.name.compareTo(user.name);  //按照姓名从小到大排列
//            return -this.name.compareTo(user.name);  //按照姓名从大到小排列
            int compare = -this.name.compareTo(user.name);  //按照姓名从大到小排列
            if(compare != 0){   //年龄从小到大排列
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        } else {
            throw new RuntimeException("输入的类型不匹配");
        }
    }
}

5.8 TreeSet的课后练习

bf48149ea63b1be479da1a19db027fb1
/**
 * MyDate类包含:
 * private成员变量year,month,day;并为每一个属性定义getter,  setter 方法;
 */
public class MyDate implements Comparable{
    private int year;
    private int month;
    private int day;
    public int getYear() {
        return year;
    }
    public void setYear(int year) {
        this.year = year;
    }
    public int getMonth() {
        return month;
    }
    public void setMonth(int month) {
        this.month = month;
    }
    public int getDay() {
        return day;
    }
    public void setDay(int day) {
        this.day = day;
    }
    public MyDate() {
    }
    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
    @Override
    public String toString() {
        return "MyDate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }
    @Override
    public int compareTo(Object o) {
        if(o instanceof MyDate){
            MyDate m = (MyDate)o;
            
            //比较年
            int minusYear = this.getYear() - m.getYear();
            if(minusYear != 0){
                return minusYear;
            }
            //比较月
            int minusMonth = this.getMonth() - m.getMonth();
            if(minusMonth != 0){
                return minusMonth;
            }
            //比较日
            return this.getDay() - m.getDay();
        }
        throw new RuntimeException("传入的数据类型不一致!");
    }
}
/**
 * 定义一个Employee类。
 * 该类包含:private成员变量name,age,birthday,
 * 其中birthday 为MyDate 类的对象;
 * 并为每一个属性定义getter, setter 方法;
 * 并重写toString 方法输出name, age, birthday
 */
public class Employee implements Comparable{
    private String name;
    private int age;
    private MyDate birthday;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public MyDate getBirthday() {
        return birthday;
    }
    public void setBirthday(MyDate birthday) {
        this.birthday = birthday;
    }
    public Employee() {
    }
    public Employee(String name, int age, MyDate birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }
    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }
    //按name排序
    @Override
    public int compareTo(Object o){
        if(o instanceof Employee){
            Employee e = (Employee)o;
            return this.name.compareTo(e.name);
        }
//        return 0;
        throw new RuntimeException("传入的数据类型不一致");
    }
}

import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;

/**
 * 创建该类的5 个对象,并把这些对象放入TreeSet 集合中
 * (下一章:TreeSet 需使用泛型来定义)分别按以下两种方式
 * 对集合中的元素进行排序,并遍历输出:
 *
 * 1). 使Employee 实现Comparable 接口,并按name 排序
 * 2). 创建TreeSet 时传入Comparator对象,按生日日期的先后排序。
 */
public class EmployeeTest {
    //问题二:按生日日期的先后排序
    @Test
    public void test2(){
        TreeSet set = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Employee && o2 instanceof Employee){
                    Employee e1 = (Employee)o1;
                    Employee e2 = (Employee)o2;

                    MyDate b1 = e1.getBirthday();
                    MyDate b2 = e2.getBirthday();

                    //方式一:
//                    //比较年
//                    int minusYear = b1.getYear() - b2.getYear();
//                    if(minusYear != 0){
//                        return minusYear;
//                    }
//                    //比较月
//                    int minusMonth = b1.getMonth() - b2.getMonth();
//                    if(minusMonth != 0){
//                        return minusMonth;
//                    }
//                    //比较日
//                    return b1.getDay() - b2.getDay();

                    //方式二:
                    return b1.compareTo(b2);
                }
//                return 0;
                throw new RuntimeException("传入的数据类型不一致!");
            }
        });

        Employee e1 = new Employee("wangxianzhi",41,new MyDate(334,5,4));
        Employee e2 = new Employee("simaqian",43,new MyDate(-145,7,12));
        Employee e3 = new Employee("yanzhenqin",44,new MyDate(709,5,9));
        Employee e4 = new Employee("zhangqian",51,new MyDate(-179,8,12));
        Employee e5 = new Employee("quyuan",21,new MyDate(-340,12,4));

        set.add(e1);
        set.add(e2);
        set.add(e3);
        set.add(e4);
        set.add(e5);

        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
    //问题一:使用自然排序
    @Test
    public void test(){
        TreeSet set = new TreeSet();

        Employee e1 = new Employee("wangxianzhi",41,new MyDate(334,5,4));
        Employee e2 = new Employee("simaqian",43,new MyDate(-145,7,12));
        Employee e3 = new Employee("yanzhenqin",44,new MyDate(709,5,9));
        Employee e4 = new Employee("zhangqian",51,new MyDate(-179,8,12));
        Employee e5 = new Employee("quyuan",21,new MyDate(-340,12,4));

        set.add(e1);
        set.add(e2);
        set.add(e3);
        set.add(e4);
        set.add(e5);

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

5.9 Set课后两道面试题

练习:在List内去除重复数字值,要求尽量简单
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

public class CollectionTest {
    //练习:在List内去除重复数字值,要求尽量简单
    public static List duplicateList(List list) {
        HashSet set = new HashSet();
        set.addAll(list);
        return new ArrayList(set);
    }
    @Test
    public void test2(){
        List list = new ArrayList();
        list.add(new Integer(1));
        list.add(new Integer(2));
        list.add(new Integer(2));
        list.add(new Integer(4));
        list.add(new Integer(4));
        List list2 = duplicateList(list);
        for (Object integer : list2) {
            System.out.println(integer);
        }
    }
    @Test
    public void test3(){
        HashSet set = new HashSet();
        Person p1 = new Person(1001,"AA");
        Person p2 = new Person(1002,"BB");

        set.add(p1);
        set.add(p2);
        System.out.println(set);

        p1.name = "CC";
        set.remove(p1);
        System.out.println(set);
        set.add(new Person(1001,"CC"));
        System.out.println(set);
        set.add(new Person(1001,"AA"));
        System.out.println(set);

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

    }
    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (id != person.id) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }
    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }
}

6、Map接口

6.1 Map接口及其多个实现类的对比

(1)Map的实现类的结构:
Map:双列数据,存储key-value对的数据 —类似于高中的函数:y = f(x)
HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历
原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素
对于频繁的遍历操作,此类执行效率高于HashMap
TreeMap:保证按照添加的key-value对进行排序,实现排序遍历,此时考虑key的自然排序或定制排序
底层使用红黑树
Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
Properties:常用来处理配置文件。key和value都是String类型

(2)HashMap的底层:数组+链表 (jdk7及之前)
数组+链表+红黑树 (jdk 8)

(3)面试题:HashMap的底层实现原理?
HashMap 和 Hashtable的异同?
CurrentHashMap 与 Hashtable的异同?(暂时不讲)

6.2 Map中存储的key-value的特点

(1)特点
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接口使用频率最高的实现类

(2)Map结构的理解
Map中的key:无序的、不可重复的,使用Set存储所有的key
key所在的类要重写equals()和hashCode() (以HashMap为例)
Map中的value:无序的、可重复的,使用Collection存储所有的value,value所在的类要重写equals()
一个键值对:key-value构成了一个Entry对象
Map中的entry:无序的、不可重复的,使用Set存储所有的entry
img

6.3 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

6.4 HashMap的底层实现原理

​ JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
​ JDK 8版本发布以后:HashMap是数组+链表+红黑树实现

HashMap源码中的重要常量:
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rwv8BVq9-1645788496345)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\58c89db281ee84d93098a4f043dd3856.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpmfzR9t-1645788496346)(C:\Users\13520\AppData\Roaming\Typora\typora-user-images\b2aebd46ed8d41111825cd15ac2a3be6.png)]

6.4.1 HashMap在JDK7中的底层实现原理

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

​ 补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储

         在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容
         默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来

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

6.4.2 HashMap在JDK8中的底层实现原理

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

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

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

​ 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会
​ 先扩容解决,如果已经达到了64,那么这个链会变成红黑树,结点类型由Node变成TreeNode类型
​ 当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把红黑树再转为链表

(4)关于映射关系的key是否可以修改?answer:不要修改
映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或
Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个
属性又参与hashcode值的计算,那么会导致匹配不上

(5) jdk8 相较于jdk7在底层实现方面的不同:
①new HashMap():底层没有创建一个长度为16的数组
②jdk 8底层的数组是:Node[],而非Entry[]
③首次调用put()方法时,底层创建长度为16的数组
④jdk7底层结构只有:数组+链表
jdk8中底层结构:数组+链表+红黑树
形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,
此时此索引位置上的所数据改为使用红黑树存储

6.5 LinkedHashMap的底层实现原理(了解)

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

342cf58820c14e037baaadd31c600d80
​ LinkedHashMap中的内部类:Entry
edbc291a09efb65428640308c90d7b8b

 *    源码中:
 *        static class Entry<K,V> extends HashMap.Node<K,V> {
 *            Entry<K,V> before, after;//能够记录添加的元素的先后顺序
 *            Entry(int hash, K key, V value, Node<K,V> next) {
 *               super(hash, key, value, next);
 *            }
 *        } 
import org.junit.Test;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class MapTest {
    @Test
    public void test2(){
        Map map = new HashMap();
        map = new LinkedHashMap();
        map.put(123,"AA");
        map.put(345,"BB");
        map.put(12,"CC");
        System.out.println(map);
    }
}

6.6 Map中的常用方法1

添加、删除、修改操作:
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集合

import org.junit.Test;
import java.util.*;

public class MapTest {
    @Test
    public void test4(){
        Map map = new HashMap();
        map.put("AA",123);
        map.put(45,123);
        map.put("BB",56);
        // Object get(Object key)
        System.out.println(map.get(45));
        //containsKey(Object key)
        boolean isExist = map.containsKey("BB");
        System.out.println(isExist);

        isExist = map.containsValue(123);
        System.out.println(isExist);

        map.clear();

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

    /**
     * 添加、删除、修改操作:
     *  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中的所有数据
     */
    @Test
    public void test3(){
        Map map = new HashMap();
        //添加
        map.put("AA",123);
        map.put(45,123);
        map.put("BB",56);
        //修改
        map.put("AA",87);

        System.out.println(map);

        Map map1 = new HashMap();
        map1.put("CC",123);
        map1.put("DD",456);
        map.putAll(map1);

        System.out.println(map);

        //remove(Object key)
        Object value = map.remove("CC");
        System.out.println(value);
        System.out.println(map);

        //clear()
        map.clear();//与map = null操作不同
        System.out.println(map.size());
        System.out.println(map);
    }
}

6.7 Map中的常用方法2

6.8 TreeMap两种添加方式的使用

6.9 Hashtable

6.10 Properties处理属性文件

7、Collections工具类

7.1 Collections工具类常用方法的测试

7.2 补充:Enumeration(了解!!!)

五、泛型

六、IO流

七、网络编程

八、反射与动态代理

九、Java8新特性

十、Java9&10&11新特性

十一、Java虚拟机JVM

​ 内存模型、GC垃圾回收,括分代,GC算法,收集器、类加载和双亲委派、JVM调优,内存泄漏和内存溢出)

  1. 垃圾回收机制关键点:
    (1)垃圾回收机制只回收JVM堆内存里的对象空间
    对其他物理连接,比如数据库连接、输入输出流、Socket连接无能为力
    (2)垃圾回收发生具有不可预知性(即什么时候回收的时间不确定),程序无法精确控制垃圾回收机制执行
    (3)可以将对象的引用变量设置为null,暗示垃圾回收机制可以回收该对象(等待回收,但时间不确定)
    (4)垃圾回收机制回收任何对象之前,总会先调用它的finalize方法(如果覆盖该方法,让一个新的引用
    对象重新引用该对象,则会重新激活对象)(默认Object类的,但子类可重写)
    (5)程序员可以通过System.gc()或者Runtime.getRuntime().gc()来通知系统进行垃圾回收,会有一些效果,
    但是系统是否进行垃圾回收依然不确定
    (6)永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值