Java 学习日记(2)

Java 学习日记(2)

异常处理

异常:在 Java 语言中,将程序执行中发生的不正常情况称为异常。

Java 程序在执行过程中所发生的异常事件可分为两类:

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

Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。

  • 空指针异常

  • 试图读取不存在的文件

  • 网络连接中断

  • 数组角标越界


package com.an.answer;

public class ErrorTest {
	public static void main(String[] args) {
		// StackOverflowError
		// main(args);
		
		// OOM
		// Integer[] arr = new Integer[2000*2000*2000];
	}
}

对于这些错误,一般有两种解决方法:一是遇到错误就终止程序的运行,另一种方法是由程序员在编写时,就考虑到错误的检测、错误消息的提示以及错误的处理。

捕获错误最理想的是在编译器期间,但是有的错误只有在运行时才发生,比如:除数为 0。

分类:编译时异常和运行时异常。

package com.an.answer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Date;
import java.util.Scanner;

import org.junit.Test;

/**
 * 一、异常体系结构
 * java.lang.Throwable
 * 		|---- java.lang.Error: 一般不编写针对性代码处理
 * 		|---- java.lang.Exception: 可以进行异常处理
 * 			|---- 编译时异常(checked)
 * 				|---- IOException 
 * 					|---- FileNotFoundException
 * 				|---- ClassNotFoundException
 * 			|---- 运行时异常(unchecked)
 * 				|---- NullPointerException
 * 				|---- ArrayIndexOutOfBoundsException
 * 				...
 * 
 * @author an
 *
 */

public class ExceptionTest {
	//NullPointerException
	@Test
	public void test1() {
		int[] arr = null;
		System.out.println(arr[0]);
	}
	//ArrayIndexOutOfBoundsException
	@Test
	public void test2() {
		int[] arr = new int[3];
		System.out.println(arr[3]);
	}
	//ClassCastException 
	@Test
	public void test3() {
		Object obj = new Date();
		String string = (String)obj;
	}
	//NumberFormatException
	@Test
	public void test4() {
		String str = "abc";
		int num = Integer.parseInt(str);
	}
	//InputMismatchException
	@Test
	public void test5() {
		Scanner scanner = new Scanner(System.in);
		int sc = scanner.nextInt();
		System.out.println(sc);
	}
	//ArithmeticException
	@Test
	public void test6() {
		int a = 10;
		int b = 0;
		System.out.println(a/b);
	}
	
	
	/**** 编译时异常 
	 * @throws FileNotFoundException *****/
	@Test
	public void test7() {
		File file = new File("hello.txt");
		FileInputStream fileInputStream = new FileInputStream(file);
		int data = fileInputStream.read();
		while(data != -1) {
			System.out.println((char)data);
			data = fileInputStream.read();
		}
		fileInputStream.close();
	}
}

在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行 x/y 运算时,要检测分母为 0,数据为空等。过多的 if-else 分支会导致程序代码过长、臃肿、可读性差。因此采用异常检测机制。

方式1: try-catch-finally

方式2: throws + 异常类型

Java 提供的是异常处理的抓抛模型。

Java 程序的执行过程中,如果出现异常,会生成一个异常对象,该异常对象将被交给 Java 运行时系统,这个过程称为抛出(thow)异常。

异常对象的生成:

  1. 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个对于异常类的实例对象并抛出。

  2. 由开发人员手动创建: Exception exception = new ClassCastException(); 创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样。

package com.an.answer;

import org.junit.Test;

/**
 * 异常处理: 抓抛模型
 * 
 * 过程一: "抛",程序在正常执行过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象
 * 并将此对象抛出。一旦抛出对象以后,其后的代码不再执行。
 * 
 * 过程二: "抓",异常处理的方式,try-catch-finally,throws
 * 
 * try {
 *  可能出现异常的代码
 *  }catch(异常类型1 变量名1) {
 *  处理异常的方法
 *  }catch(异常类型2 变量名2) {
 *  }
 *  ...
 *  finally {
 *  	一定会执行的代码,可选
 *  }
 *  
 *  说明:
 *  1、finally 是可选的
 *  2、使用 try 将可能出现异常代码包装起来,在执行过程中,一旦出现异常,就会
 *  生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配
 *  3、一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常处理,一旦
 *  处理完成,就跳出当前的 try-catch 结构(在没写finally的情况下)。继续执行之后的
 *  代码。
 *  4、catch中的异常类型,如果没有子父类关系,声明的位置无所谓,否则子类一定要
 *  声明在父类之上。
 *  5、常用异常处理方法:① String getMessage(); ② printStackTrace();
 * @author an
 *
 */

public class ExceptionTest1 {
	@Test
	public void test1() {
		try {
			String str = "abc";
			int num = Integer.parseInt(str);
			System.out.println("Hello");
		} catch(NumberFormatException e) {
			System.out.println("出现数值转换异常");
			
			// String getMessage();
			System.out.println(e.getMessage());
			// printStackTrace();
			e.printStackTrace();
		}
	}
}


package com.an.answer;

import org.junit.Test;

/**
 * finally 的使用
 * 
 * 1、finally 可选 
 * 2、finally 中声明的是一定会被执行的代码,即使catch中又出现异常
 * 了,try中有return 语句,catch中有return语句等情况
 * 3、像数据库连接,输入输出流,JVM是不能自动回收,需要手动进行资源释放,此时需要
 * 使用 finally
 *  
 * @author an
 *
 */
public class FinallyTest {

	@Test
	public void test1() {
		try {
			int a = 10;
			int b = 0;
			System.out.println(a / b);
		} catch (ArithmeticException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			System.out.println("finally");
		}
	}
}


package com.an.answer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * 异常处理的方式二: throws + 异常类型
 * 
 * 1、声明在方法的声明处,指明此方法执行时会抛出的异常类型,一旦当方法体
 * 执行时,出现异常,仍会在异常代码执行处生成一个异常类对象,此对象满足throws
 * 后异常类型时,就会抛出。异常后续的代码,不会再执行。
 * 2、try-catch-finally: 真正处理异常
 * throws: 只是将异常抛给方法的调用者,并没有真正将异常处理掉
 * 3、开发中如何选择 try-catch-finally 还是使用 throws?
 * 	> 如果父类中被重写的方法没有throws 方式处理异常,则子类重写的方法也不能throws
 * 如果子类重写的方法中有异常,必须使用try-catch-finally方式。
 *  > 执行的方法中,先后调用了几个方法,这几个方法是递进关系执行的,建议对
 *  这几个方法使用throws,最后集中try-catch
 * @author an
 *
 */

public class ExceptionTest2 {
	
	public static void main(String[] args) {
		try {
			method2();
		}catch (FileNotFoundException e) {
			e.printStackTrace();
		}catch(IOException e) {
			e.printStackTrace();
		}
	}
	
	public static void method2() throws FileNotFoundException,IOException {
		method1();
	}
	
	
	public static void method1() throws FileNotFoundException,IOException {
		File file = new File("hello.txt");
		FileInputStream fileInputStream = new FileInputStream(file);
		int data = fileInputStream.read();
		while(data!=-1) {
			System.out.println((char)data);
			data = fileInputStream.read();
		}
		fileInputStream.close();
	}
}

package com.an.answer;

import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * 方法重写的规则之一:
 * 子类重写的方法抛出的异常不大于父类被重写方法抛出的异常类型
 * @author an
 *
 */

public class OverrideTest {
	
	public static void main(String[] args) {
		OverrideTest test = new OverrideTest();
//		test.display(test);
	}
	
	public void display(SuperClass s) {
		try {
			s.method();
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
}

class SuperClass {
	public void method() throws IOException {
		
	}
	
}
class SubClass extends SuperClass {
	public void method() throws FileNotFoundException {
		
	}
}

手动抛出异常

关于异常对象的产生:① 系统自动生成的异常对象 ②手动的生成一个异常对象,并抛出(throw)

package com.an.answer;

public class StudentTest {
	public static void main(String[] args) {
		Student st = new Student();
		try {
			st.regist(-1001);
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

}

class Student {
	private int id;

	public void regist(int id) throws Exception {
		if (id > 0) {
			this.id = id;
		} else {
			// System.out.println("输入的数据非法");
			// throw new RuntimeException("输入的数据非法");
			throw new Exception("输入的数据非法");
		}
	}
}

用户自定义异常类

package com.an.answer;

/**
 * 如何自定义异常类
 * 1、继承于现有的异常结构,RuntimeException,Exception
 * 2、提供全局变量:serialVersionUID
 * 3、重载的构造器
 * @author an
 *
 */


public class MyException extends RuntimeException {
	static final long serialVersionUID = 7845237528546090155L;

	public MyException() {
		
	}
	
	public MyException(String msg) {
		super(msg);
	}

}

多线程基本概念

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

进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程(生命周期)。
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

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

单核和多核CPU:
单核CPU,是一种假的多线程,因为在一个时间单元内,只能执行一个线程的任务。例如:虽然有多条车道,但是收费站只有一个工作人员在收费,只有收费才能通过。那么CPU就相当于收费人员。如果有某个人不想交钱,那么收费人员可以把它"挂起"。但是因为CPU时间单元特别短,因此感觉不出来。
如果是多核的话,才能更好的发挥多线程的效率。
一个java应用程序java.exe,其实至少有三个线程: main()主线程,gc()垃圾回收线程,异常处理线程。

并行与并发
并行:多个CPU同事执行多个任务。
并发:一个CPU(采用时间片)同时执行多个任务。

线程的创建和使用

package com.an.java;

/**
 * 多线程的创建,方式1: 继承于Thread类
 * 1、创建一个继承于Thread类的子类
 * 2、重写Thread类中的run() -> 将此线程执行的操作声明在run()中
 * 3、创建Thread类中的子类的对象
 * 4、通过此对象调用start()
 *
 * 例子:遍历100以内所有的偶数
 * @author an
 * @create 2021-04-08-20:13
 */
//1、创建一个继承于Thread类的子类
class MyThread extends Thread {
    //2、重写Thread类中的run() -> 将此线程执行的操作声明在run()中
    @Override
    public void run() {
        for(int i=0;i<10000000;i++){
            if(i%200000==0){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3、创建Thread类的子类的对象
        MyThread t1 = new MyThread();
        //4、通过此对象调用start() ①启动当前线程 ②调用当前线程的run()方法
        t1.start();
        for(int i=0;i<100;i++){
            if(i%2==0){
                System.out.println(i+"*******main()*******");
            }
        }
    }
}

package com.an.java.exer;

/**
 * 练习:创建两个分线程,一个线程遍历偶数,一个遍历奇数
 *
 * @author an
 * @create 2021-04-09-10:25
 */
public class ThreadDemo {
    public static void main(String[] args) {
//        MyThread1 m1 = new MyThread1();
//        MyThread2 m2 = new MyThread2();
//        m1.start();
//        m2.start();

        // 创建 Thread 类的匿名子类
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0)
                        System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 != 0)
                        System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();
    }
}


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

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

Thread 类的有关方法

void start():启动线程,并执行对象的run()方法。

run():线程被调度时执行的操作。

String getName():返回线程的名称。

void setName(String name):设置该线程名称。

static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类。

static void yield():线程让步

暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。

若队列中没有同优先级的线程,忽略此方法。

join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。

static void sleep(long millis):(时间:毫秒)
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。

抛出InterruptException异常。

stop():强制线程生命期结束,不推荐使用。

boolean isAlive():返回 boolean,判断线程是否还活着。

线程的调度

调度策略:

1、时间片
2、抢占式:高优先级的线程抢占CPU

Java 的调度方法

1、同优先级线程组成先进先出队列,使用时间片策略
2、对高优先级,使用优先调度的抢占式策略

线程的优先等级:

1、MAX_PRIORITY: 10
2、MIN_PRIORITY: 1
3、NORM_PRIORITY: 5

涉及的方法:

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

说明:

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

package com.an.java;

/**
 * 测试 Thread 类中的常用方法
 * 1、start() 启动当前线程,调用当前线程的 run() 方法
 * 2、run():通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中
 * 3、currentThread():静态方法,返回执行当前代码的线程
 * 4、getName():获取当前线程的名字
 * 5、setName():设置当前线程的名字
 * 6、yield():一旦线程执行此方法,释放当前cpu的执行权
 * 7、join():在线程a中调用线程b的方法,线程a进入阻塞状态,直到线程b执行结束。
 * 8、stop():已过时,不使用。
 * 9、sleep(long millitime):让当前线程“睡眠”指定的毫秒数,在这段时间内,该线程处于阻塞状态
 * 10、boolean isAlive():判断当前线程是否存活
 *
 * 线程优先级:
 * 1、
 * 1) MAX_PRIORITY: 10
 * 2) MIN_PRIORITY: 1
 * 3) NORM_PRIORITY: 5
 *
 * 2、获取和设置
 *  getPriority()
 *  setPriority()
 *
 * @author an
 * @create 2021-04-09-10:38
 */

class HelloThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                try {
                    Thread.currentThread().sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.getName() + ":" + getPriority()+":"+i);
            }
//            if (i % 4 == 0) {
//                yield();
//            }
        }
    }

    public HelloThread(String name) {
        super(name);
    }
}


public class ThreadMethodTest {
    public static void main(String[] args)  {
        Thread.currentThread().setName("主线程");
        HelloThread h1 = new HelloThread("线程1");
//        h1.setName("线程1");
        //设置分线程的优先级
        h1.setPriority(Thread.MAX_PRIORITY);
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        h1.start();
        for(int i =0 ;i<100;i++){
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if(i==20) {
                try {
                    h1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(h1.isAlive());
    }
}

使用Runnable接口来实现线程

package com.an.java;

/**
 * 创建多线程方式二:实现Runnable接口
 * 1、创建一个实现了Runnable接口的类
 * 2、实现类去实现Runnable接口中的抽象方法
 * 3、创建实现类的对象
 * 4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
 * 5、通过Thread类的对象调用start()
 *
 * 创建线程的两种方式的比较:
 * 开发中,我们优先选择实现Runnable接口的方式
 * 原因 1、实现的方式没有单继承的限制
 *      2、实现的方式更容易实现多个线程有共享数据的情况
 * 联系:public class Thread implements Runnable
 * 相同点:两种方式都需要重写run()
 *
 * @author an
 * @create 2021-04-09-14:44
 */
//1、创建一个实现了Runnable接口的类
class MThread implements Runnable {
    //2、实现类去实现Runnble中的抽象方法: run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0)
                System.out.println(i);
        }
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        //3、创建实现类的对象
        MThread mThread = new MThread();
        //4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        //5、通过Thread类的对象调用start()
        t1.start();
    }
}

线程的生命周期

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

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

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

Runnable 接口同步代码块


package com.an.java;

/**
 * 1、问题:卖票过程中,出现了重票、错票 -> 出现了线程安全问题
 * 2、问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来
 * 3、如何解决:当一个线程a操作ticket时,其他线程不能参与进来,直到线程a操作完ticket时,
 * 线程才可以开始操作ticket,这种情况即使线程a出现了阻塞,也不能被改变
 * 4、在Java中,我们通过同步机制来解决线程安全问题
 *
 * 方式1:同步代码块
 *
 * synchronized(同步监视器){
 * 需要被同步的代码
 * }
 * 说明:1、需要被同步的代码(操作共享数据的代码)
 *      2、共享数据:多个线程共同操作的变量,比如: ticket就是共享数据
 *      3、同步监视器,俗称:锁,如何一个类的对象都可以充当一个锁
 *         要求:多个线程必须要共用同一把锁
 *
 * 方式2: 同步方法
 *
 * 5、同步的方式:解决了线程安全问题
 *    操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低
 *
 *
 * @author an
 * @create 2021-04-09-15:40
 */
class Window1 implements Runnable {
    private int ticket = 100;
    Object obj = new Object();

    @Override
    public void run() {

        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                        System.out.println(ticket);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class WindowTest {
    public static void main(String[] args) {
        Window1 m1 = new Window1();
        Thread t1 = new Thread(m1);
        Thread t2 = new Thread(m1);
        Thread t3 = new Thread(m1);
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window1 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {

        while (true) {
            // 使用当前对象作为锁
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                        System.out.println(ticket);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

同步代码块继承 Thread 的锁


package com.an.java;

/**
 * 使用同步代码块解决继承Thread的方式
 *
 * @author an
 * @create 2021-04-09-16:16
 */

class Window extends Thread {

    private static int ticket = 100;
    private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    System.out.println(getName() + ":" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}


public class WindowTest2 {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window extends Thread {

    private static int ticket = 100;
    private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (Window.class) {
                if (ticket > 0) {
                    System.out.println(getName() + ":" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

同步方法

package com.an.java;

/**
 * 方式2:同步方法
 * 如果操作共享数据的代码完整声明在一个方法中,我们不妨将这个方法声明为同步的
 *
 *
 * @author an
 * @create 2021-04-09-15:40
 */
class Window3 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {

        while (true) {
            // 使用当前对象作为锁
            if(!show()) break;
        }
    }

    private synchronized boolean show() { // 同步监视器: this
        if (ticket > 0) {
            try {
                Thread.sleep(100);
                System.out.println(ticket);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket--;
            return true;
        } 
        return false;
    }
}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 m1 = new Window3();
        Thread t1 = new Thread(m1);
        Thread t2 = new Thread(m1);
        Thread t3 = new Thread(m1);
        t1.start();
        t2.start();
        t3.start();
    }
}

package com.an.java;

/**
 * 使用同步方法来处理继承Thread的方式
 *
 * @author an
 * @create 2021-04-09-16:16
 */

class Window4 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if (!show()) break;
        }
    }

    private static boolean show() { 
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ":" + ticket);
            ticket--;
            return true;
        } else {
            return false;
        }
    }
}


public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();
        t1.start();
        t2.start();
        t3.start();
    }
}

关于同步方法的总结:
1、同步方法仍然涉及到同步监视器,只是不需要我们显式声明
2、非静态的同步方法,同步监视器是 this
3、静态的同步方法,同步监视器是当前类本身

单例模式之懒汉式问题

package com.an.java1;

/**
 * 使用同步机制将单例模式中的懒汉式改写为线程安全的
 *
 * @author an
 * @create 2021-04-09-17:05
 */
public class BankTest {
}

class Bank {
    private Bank() {

    }

    private static Bank instance = null;


    public static Bank getInstance() {
        // 方式一:效率差
        // 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;
    }

}

死锁问题

解决方法:

专门的方法、原则

尽量减少同步资源的定义

尽量避免嵌套同步

package com.an.java1;

/**
 * 演示线程的死锁问题
 *
 * 1、死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
 *
 * 2、说明:
 * 1) 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
 * 2) 我们使用同步时,要避免出现死锁。
 *
 *
 * @author an
 * @create 2021-04-09-19:15
 */
public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread() {
            @Override
            public void run() {

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


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

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

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

            }
        }.start();

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

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");

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

Lock(锁)

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

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

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

package com.an.java1;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 解决线程安全的方式三:Lock 锁,--jdk 5.0
 * 1、面试题:synchronized 和 Lock 异同
 * 相同:二者都可以解决线程安全问题
 * 不同:① synchronized 机制在执行完相应的同步代码后自动释放
 *      ② Lock 需要手动启动同步,同时结束同步也需要手动实现
 *      ③ Lock 只有代码块锁,synchronized 有代码块锁和方法锁
 *
 *
 * @author an
 * @create 2021-04-09-19:38
 */


class Window implements Runnable {

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

    @Override
    public void run() {
        while (true) {
            try {

                // 2、调用 Lock() 方法
                lock.lock();

                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                } else
                    break;
            }finally {
                lock.unlock();
            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

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

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

线程的通信

例题:两个线程交替打印

package com.an.java2;

/**
 * 线程通信的例子:使用两个线程交替打印 1-100。线程1,线程2交替打印
 *
 * wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器
 * notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的
 * notifyAll():唤醒所有被wait的线程
 *
 * 说明:
 * 1、wait(),notify(),notifyAll() 这三个方法必须使用在同步代码块或同步方法中。
 * 2、三个方法的调用者必须是同步代码块或同步方法中的同步监视器
 * 3、三个方法定义在 Object 中
 *
 * @author an
 * @create 2021-04-09-19:58
 */

class Number implements Runnable {
    private int number = 1;

    @Override
    public void run() {
        while (true) {

            synchronized (this) {
                notify();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

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

                    try {
                        // 使得调用如下 wait 方法的线程进入阻塞状态
                        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.start();
        t2.start();
    }
}

面试题:

sleep() 和 wait() 方法的异同:
1、相同点:一旦执行方法,都可以使当前线程进入阻塞状态

2、不同点:

① 两个方法声明的位置不一样:Thread 类中声明 sleep(),Object 声明 wait()。
② 调用的范围不同:sleep() 方法可以在任何需要的场景下调用,wait() 必须使用在同步方法中。
③ 关于是否释放同步监视器的问题:如果两个方法都使用在同步代码块和同步方法中,sleep() 不会释放锁,wait() 会释放锁。

例题:生产者消费者问题

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

package com.an.java2;

/**
 * 线程通信应用:生产者/消费者问题
 *
 * 1、是否是多线程的问题,生产者线程,消费者线程
 * 2、是否有线程安全问题,共享数据是产品
 * 3、如何解决线程安全问题,同步机制,三种方法
 * 4、线程通信
 *
 * @author an
 * @create 2021-04-09-20:20
 */

class Clerk {

    private int productCount = 0;

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

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

class Producer extends Thread {
    private Clerk clerk;

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

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


    }
}

class Consumer extends Thread {
    private Clerk clerk;

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

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开始消费产品");
        while (true) {
            clerk.consumeConsumer();
        }
    }
}


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

        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);

        producer.start();
        consumer.start();
    }
}

Callable 创建多线程

与 Runnable 相比,Callable 更强大

相比 run() 方法,可以有返回值。

方法可以抛出异常。

支持泛型的返回值。

需要借助FutureTask类,比如获取返回结果。

package com.an.java2;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 创建线程的方式三:实现Callable。 --JDK 5.0 新增
 *
 * @author an
 * @create 2021-04-09-20:38
 */


// 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 = new FutureTask(numThread);
        //5、将FutureTask作为参数传入到Thread中
        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();
        }
    }

}

使用线程池创建线程

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

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

  • Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池。

  • Executors.newFixedThreadPool(n): 创建一个可重用固定线程数的线程池。

  • Executors.newSingle(): 创建一个只有一个线程的线程池。

  • Executors.newScheduledThreadPool(n): 创建一个线程池,可以安排在给定延迟后运行命令或定期执行。

package com.an.java2;

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

/**
 * 创建线程的方式四:使用线程池
 *
 * 好处:
 * 1、提高响应速度(减少创建线程所需的时间)
 * 2、降低资源消耗(重复利用线程池中线程)
 * 3、便于线程创建
 *
 *
 * @author an
 * @create 2021-04-09-20:56
 */

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);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        // 1、提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

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

        // 3、关闭线程池
        service.shutdown();
    }
}

String 类

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

String 是个 final 类,代表不可变的字符序列。

字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。

String 对象的字符内容是存储在一个字符数组 value[] 中的。

package com.an.java;

import org.junit.Test;

/**
 * String 的使用
 *
 * @author an
 * @create 2021-04-10-19:15
 */
public class StringTest {
    /*
    String: 字符串,使用一对""引起来表示
    1、String 声明为 final 的,不可被继承
    2、String 实现了 Serializable 接口:表示字符串是支持序列化的
             实现了 Comparable 接口:表示 String 可以比较大小
    3、String 内部定义了 final char[] value 用于存储字符串数据
    4、String: 代表一个不可变的字符序列,简称:不可变性。
       体现 1. 对字符串重新赋值时,需要重写指定的内存区域赋值,不能使用原有的位置
            2. 当对现有的字符串进行连接操作时,也需要重写指定新的内存区域
            3. 当调用 String 的 replace 方法修改指定的字符串时,需要重新指定内存区域
    5、通过字面量的方式给一个字符串赋值,此时的字符串值声明在常量池中
    6、字符串常量池中不会存储相同内容的字符串

     */

    @Test
    public void test1() {
        String s1 = "abc"; //字面量
        String s2 = "abc";

        System.out.println(s1 == s2); // true

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

        String s3 = "a";
        s3 += "bc";

        System.out.println(s3);

        String s4 = "abc";
        String s5 = s4.replace('a', 'm');

        System.out.println(s5);
    }


    /*
    String 实例化方式
    方式一:通过字面量定义的方式
    方式二:通过 new + 构造器的方式
     */

    @Test
    public void test2() {
        // 此时s1和s2的数据声明在方法区的字符串常量池中
        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(s3 == s4); //false;

        Person p1 = new Person("tom", 12);
        Person p2 = new Person("tom", 12);
        System.out.println(p1.name == p2.name); //true
    }


    /*
        常量与常量的拼接结果在常量池,且常量池中不会存在相同内容的常量。
        只要其中有一个是变量,结果就在堆中
        如果拼接的结果调用intern()方法,返回值就在栈中
     */
    @Test
    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
        System.out.println(s3 == s6); //false
        System.out.println(s5 == s6); //false
        System.out.println(s5 == s7); //false

        String s8 = s5.intern();

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


}

String 中的方法

package com.an.java;

import org.junit.Test;

/**
 * @author an
 * @create 2021-04-10-20:04
 */
public class StringMethodTest {
    @Test
    public void test1() {
        String s1 = "HelloWorld";

        // 长度、索引位置、是否为空
        System.out.println(s1.length());
        System.out.println(s1.charAt(1));
        System.out.println(s1.isEmpty());

        // 大小写转换
        System.out.println(s1.toLowerCase());
        System.out.println(s1.toUpperCase());

        // 返回字符串副本,忽略前导和尾部空格
        String s2 = "  Hello World   ";
        System.out.println(s2.trim());

        // 不忽略/忽略大小写比较大小,取第一个不相等的字母的ASCII码差值作为返回值
        String s3 = "Hello";
        String s4 = "hello";
        System.out.println(s3.equals(s4));
        System.out.println(s3.equalsIgnoreCase(s4));

        // concat 连接,等价于+
        String s5 = s3.concat(s4);
        System.out.println(s5);

        // compareTo 比较两个字符串大小
        System.out.println(s3.compareTo(s4));

        // substring(int beginIndex)
        // substring(int beginIndex,int endIndex) 左闭右开
        System.out.println(s5.substring(3,6));

    }

    @Test
    public void test2() {
        // endsWith 是否以指定字符串结束
        String s1 = "HelloWorld";
        String s2 = "World";
        System.out.println(s1.endsWith(s2));

        String s3 = "Hello";
        System.out.println(s1.startsWith(s3));

        // 是否包含char序列
        System.out.println(s1.contains(s2));


        // indexOf() indexOf(String,startIndex); 找不到返回 -1
        System.out.println(s1.indexOf(s3));
    }

    /*
    String replace(old char/str,new char/str);



     */


    @Test
    public void test3() {
        String s1 = "HelloWorld";
        String s2 = "World";
        System.out.println(s1.replace("Wor","i"));
    }

}

String 与 char[] 和 byte[] 转换

package com.an.java;

import org.junit.Test;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;

/**
 * @author an
 * @create 2021-04-10-20:23
 */
public class StringTest1 {
    @Test
    public void test1() {
        String str = "abc123";
        char[] charArray = str.toCharArray();
        for(int i=0;i<charArray.length;i++) {
            System.out.println(charArray[i]);
        }
        String str2 = new String(charArray);

        byte[] bytes = str.getBytes(); // 使用默认的字符集进行转换;
        System.out.println(Arrays.toString(bytes));

        try {
            byte[] bytes2 = str.getBytes("gbk");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        try {
            String str3 = new String(bytes,"gbk");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }


    }
}

StringBuffer 和 StringBuilder

package com.an.java;

import org.junit.Test;

/**
 * @author an
 * @create 2021-04-10-20:31
 */
public class StringBufferBuilderTest {

    /*
    String: 不可变的字符序列
    StringBuffer: 可变的字符序列,线程安全,效率偏低
    StringBuilder: 可变的字符序列,线程不安全,效率高

    源码分析:
    String str = new String(); // new char[0];
    String str = new String("abc"); // new char[]{'a','b','c'}

    StringBuffer sb1 = new StringBuffer(); // char[] value = new char[16]
    sb1.append('a'); //value[0]='a'
    sb1.append('b'); //value[1]='b'

    StringBuffer sb2 = new StringBuffer("abc"); // char[] value = new char["abc".length()+16]


    // 问题 1: System.out.println(sb2.length()); //3
    // 问题 2: 扩容问题:如果添加的数据底层数组盛不下了,需要扩容底层数组
    // 扩容为原来的2倍+2,同时将原有数组的元素复制到新数组中

     */

    @Test
    public void test1() {
        StringBuffer sb1 = new StringBuffer("abc");
        sb1.setCharAt(0,'m');
        System.out.println(sb1);

        sb1.append("cc");
        System.out.println(sb1);

        sb1.reverse();
        System.out.println(sb1);

        sb1.delete(1,2);
        System.out.println(sb1);

        sb1.insert(0,"st");
        System.out.println(sb1);

        sb1.replace(0,2,"xxe");
        System.out.println(sb1);
    }

}

JDK8 之前日期时间API

package com.an.java;

import org.junit.Test;

import java.util.Date;

/**
 * JDK 8 之前日期和时间的API测试
 *
 * @author an
 * @create 2021-04-10-21:34
 */
public class DateTimeTest {

    @Test
    public void test1() {
        // 返回当前时间与 1970.1.1 0:0:0 的时间差(ms) 时间戳
        System.out.println(System.currentTimeMillis());
    }


    /*
    java.util.Date 类
          |-- java.sql.Date类
    1、两个构造器的使用

    2、两个方法的使用
         > toString() 显式当前年月日时分秒
         > getTime() 毫秒数,时间戳

    3、java.sql.Date 对应数据库
         > 如何实例化
         > sql.Date -> util.Date
         > util -> sql.Date
     */


    @Test
    public void test2() {

        // 构造器1:创建一个对应当前时间的Date对象
        Date date1 = new Date();
        System.out.println(date1.toString());
        System.out.println(date1.getTime());

        // 构造器2:创建一个指定时间的构造器
        Date date2 = new Date(1029134003094L);
        System.out.println(date2.toString());


        java.sql.Date date3 = new java.sql.Date(1029134003094L);
        System.out.println(date3.toString());

        Date date6 = new Date(date3.getTime());
    }
}

DateFormat、Calender

package com.an.java;

import org.junit.Test;

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

/**
 *
 * 1、SimpleDateFormat
 * 2、Calender
 * @author an
 * @create 2021-04-12-20:00
 */
public class DateTimeTest1 {
    /*
    SimpleDateFormat 的使用: SimpleDateFormat 对日期Date类的格式化和解析
    1、两个操作:
      > 格式化: 日期 -> 字符串
      > 解析:字符串 -> 日期
    2、SimpleDateFormat 的实例化

     */
    @Test
    public void testSimpleDateFormat() {
        // 实例化SimpleDateFormat
        SimpleDateFormat sdf = new SimpleDateFormat();

        // 格式化
        Date date = new Date();

        String format = sdf.format(date);

        System.out.println(format);

        // 解析
        String str = "2021/4/12 下午8:09";
        try {
            Date date1 = sdf.parse(str);
        } catch (ParseException e) {
            e.printStackTrace();
        }


    }

    /*
    Calender 日历类(抽象类)的使用
     */


    @Test
    public void calenderTest() {
        // 1、实例化
        // 方式1:创建其子类的对象
        // 方式2:调用其静态方法
        Calendar calender = Calendar.getInstance();
        System.out.println(calender.getClass());

        // 2、常用方法
        // get()
        int days = calender.get(Calendar.DAY_OF_MONTH);
        System.out.println(days);
        // set()
        calender.set(Calendar.DAY_OF_MONTH,22);
        

        // add()
        calender.add(Calendar.DAY_OF_MONTH,3);
        // getTime()
        Date time = calender.getTime();

        // setTime()
        calender.setTime(time);

    }
}


Java 比较器

package com.an.java;

import org.junit.Test;

import java.util.Arrays;
import java.util.Comparator;

/**
 * 一、说明: Java 中的对象,正常情况下,只能进行比较操作: == !=,
 * 不能使用 > <,但是在开发场景中,我们需要对多个对象进行排序,就需要比较对象大小
 * 如何实现?使用两个接口的任何一个 Comparable, Comparator
 *
 * 二、Comparable 接口的使用, 自然排序
 * @author an
 * @create 2021-04-12-20:21
 */
public class CompareTest {
    /*
    Comparable 使用举例
    1、像 String,包装类等实现了Comparable接口,重写了compareTo()方法,
    给出了比较两个对象大小的规则
    2、像 String,包装类重写compareTo()包装类后从小到大
    3、重写compareTo()的规则:
       如果当前对象大于形参对象,返回正整数,否则返回负数,等于返回0
    4、对于自定义类来说,如果需要重排序,需要重写 compareTo() 方法
     */

    @Test
    public void test1() {
        String[] arr = new String[]{"a","ba","aa"};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    @Test
    public void test2() {
        Goods[] arr = new Goods[3];
        arr[0] = new Goods("aa",34);
        arr[1] = new Goods("ba",21);
        arr[2] = new Goods("ab",32);

        Arrays.sort(arr);

        System.out.println(Arrays.toString(arr));
    }


    /*
    Comparator 接口的使用: 定制排序
    1、背景:
    当元素的类型没有实现Comparable接口或实现的方法不适合当前操作,那就使用Comparator
    2、重写compare(o1,o2)方法
     */
    @Test
    public void test3() {
        String[] arr = new String[]{"a","ba","aa"};
        Arrays.sort(arr, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return -o1.compareTo(o2);
            }
        });
        System.out.println(Arrays.toString(arr));
    }

}

class Goods implements Comparable{
    private String name;
    private double price;

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

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

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

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

枚举类

类的对象只有有限个。

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

/**
 * 一、枚举类的使用
 *
 * 1、类的对象只有有限个。
 *
 * 2、当需要定义一组常量时,使用枚举类。
 *
 * 3、如果枚举类中只有一个对象,可以作为单例模式的实现
 *
 * 二、如何定义枚举类
 *
 * 方式1、JDK 5.0 之前自定义枚举类
 *
 * 方式2、JDK 5.0,可以使用Enum关键字
 *
 * 三、Enum 类中的常用方法
 *
 * values(): 返回枚举类型的对象数组,该方法可以方便遍历所有枚举值
 * valueOf(String str): 根据ObjName 返回与ObjName同名的对象
 *
 * 四、使用 Enum 关键字实现接口
 *  1、实现接口,在enum类中实现抽象方法
 *  2、让枚举类的对象分别实现接口
 * toString()
 *
 * @author an
 * @create 2021-04-12-20:45
 */
public class SeasonTest {
    public static void main(String[] args) {
        Season spring = Season.SPRING;

        Season1[] values = Season1.values();

        // valueOf
        Season1 winter = Season1.valueOf("WINTER");
    }
}

// 自定义枚举类
class Season {
    // 1、声明 Season对象的属性,final private
    final private String seasonName;
    final private String seasonDesc;

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

    // 3、提供当前枚举类的多个对象
    public static final Season SPRING = new Season("春天","1");
    public static final Season SUMMER = new Season("夏天","2");
    public static final Season AUTUMN = new Season("秋天","3");
    public static final Season WINTER = new Season("冬天","4");


    // 4、获取枚举对象的属性


    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    // 5、提供 toString 方法


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

// enum
interface Info {
    void show();
}


enum Season1 implements Info{

    SPRING("春天","1"){
        public void show() {
            System.out.println("春天");
        }

    },
    SUMMER("夏天","2"){
        public void show() {
            System.out.println("夏天");
        }
    },
    AUTUMN("秋天","3"){
        public void show() {
            System.out.println("秋天");
        }
    },
    WINTER("冬天","4"){
        public void show() {
            System.out.println("冬天");
        }
    };



    final private String seasonName;
    final private String seasonDesc;

    private Season1(String seasonName,String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }

    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

//    @Override
//    public void show() {
//        System.out.println("季节");
//    }
}

注解

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

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

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

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

示例一:生成文档时的注解

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

@version 标明该类模块的版本

@see 参考转向

@since 从哪个版本开始增加

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

格式要求 @param+形参名+形参列表+形参说明

@return 对方法返回值的说明

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

@exception 对方法可能抛出的异常说明

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

后面三个用于方法

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

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

@Deprecated:用于表示所修饰的方法已经过时

@SuppressWarning:抑制编译器警告

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

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

自定义注解

/**
 * 1、注解声明为 @interface
 * 2、自定义注解自动继承了 java.lang.annotation.Annotation 接口
 * 3、Annotation 成员变量在 Annotation 定义中以无参数方法的形式来声明,
 * 其方法名和返回值定义了该成员的名字和类型,我们称之为配置参数。类型只能
 * 为8中基本数据类型、String类型、Class类型、enum 类型、Annotation 类型
 * 和对应的数组
 * 4、可以在定义成员变量时使用 default 关键字指定初始值
 * 5、如果只有一个参数,建议使用参数名为value
 * 6、没有成员变量的Annotation 称为标记,有成员变量的Annotation 称为元数据
 * @author an
 * @create 2021-04-13-13:30
 */
public @interface MyAnnotation {
    String value() default "Hello";
}

/**
 *
 * 自定义: 参照 suppressWarning 定义
 * @author an
 * @create 2021-04-13-13:29
 */
@MyAnnotation(value="Hello")
public class AnnotationTest {

}


JDK 中的元注解

JDK 的元 Annotation 用于修饰其他 Annotation 定义。

  • Retention

  • Target

  • Documented

  • Inherited

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

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.TYPE;

/**
 *
 * 自定义: 参照 suppressWarning 定义, 通常会指定两个注解 Retention,Target
 *
 * 元注解:对现有的注解进行解释说明
 *
 * > @Retention: 只能用来修饰一个 Annotation 定义,用于指定该 Annotation
 * 生命周期,@Retention 包含一个 RetentionPolicy 类型的成员变量,使用时
 * 必须为该value 指定值。
 *   >> RetentionPolicy.SOURCE: 在源文件中有效,编译器直接丢弃这种策略的注释
 *   >> RetentionPolicy.CLASS: 默认值,在class文件中有效,运行时直接丢弃
 *   >> RetentionPolicy.RUNTIME: 在运行时有效,程序可以通过反射获取此注解
 * > @Target: 指定被修饰的Annotation能用于修饰那些程序元素,@Target包含一个名为value的成员变量
 *   >> CONSTRUCTOR 描述构造器
 *   >> PACKAGE
 *   >> FIELD
 *   >> PARAMETER
 *   >> LOCAL_VARIABLE
 *   >> TYPE
 *   >> METHOD
 * **************低频率****************
 * > @Documented: 被该Annotation修饰的Annotation类将被javadoc工具提取成
 * 文档
 * > @Inherited: 被它修饰的Annotation 具有继承性
 * @author an
 * @create 2021-04-13-13:29
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE,FIELD})
public @interface MyAnnotation {
    String value() default "Hello";
}

JDK 8 新特性

1、重复注解 @Repeatable(MyAnnotation.class)

2、类型注解

TYPE_PARAMETER,TYPE_USE

posted @ 2021-04-07 21:22  ans20xx  阅读(86)  评论(0编辑  收藏  举报