一、标识符
以字母、下划线、美元符号$开头,后面接字母、下划线、美元符号$、数字。
注意:数字不能开头
保留字/关键字不能作为标识符
二、数据类型转换
- 自动转换
java可以自动对某些数据类型进行自动转换。规则:只能由低字节向高字节进行转换,反之则不行
数据类型 字节数
byte 1
short 2
int 4
long 8
float 4
double 8
char 2
boolean 1/8
例如:可以由int转换成long;对于同字节转换的,例如int和float之间转换。int可以直接转换为float,但是float必须强制转换为Int。他们虽然字节数相同,但是float可以带有小数位,更为精确。
- 强制类型转换
一般来讲强制类型转换可能会造成精度损失。
3、逻辑运算符
&(与)、|(或)、!(非)、&&(短路与)、||(短路或)
短路与比与运算效率高。例如a&b 和a&&b,前1个会对a和b的值进行判断(即使a为false也会进行判断),而后一个则在a为false的时候,不会对b进行判断,效率更高。
public class application {
public static void main(String[] args){
sum1(10, 11);
sum2(10, 11);
}
//与运算
public static void sum1(int a, int b){
System.out.println((a++ == b) & (++a == b));
System.out.println(a);
System.out.println(b);
}
//短路与运算
public static void sum2(int a, int b){
System.out.println((a++ == b) && (++a == b));
System.out.println(a);
System.out.println(b);
}
}
--结果
false
12
11
false
11
11
移位运算
- 变量1<<变量2,表示变量1* 2的变量2次方。例如2<<3=2*8=16
- 变量2>>变量2,表示变量1 / 2的变量2次方。例如2>>3=2/8=0
运算符优先级
!(非运算) > 算术运算符(加减乘除、大于、小于、大于等于、小于等于、不等于) > 逻辑运算符(&& > ||,与运算大于或运算)
三、冒泡排序
public class application {
public static void main(String[] args){
int[] arr = {10, 54, 23, 87, 98, 25};
sort(arr);
}
public static void sort(int[] arr){
int n = arr.length;
System.out.println("排序前;");
for(int i=0; i<n; i++){
System.out.println(arr[i]);
}
System.out.println("排序后;");
int temp;
//n个数,只需要进行n-1次排序,所有数据就会变成有序,所以i是从0~n-2即n-1次排序
//j每次都是从0(从头开始),然后选择前后两个数进行比较。前面的大于后面则交换位置,小于则不动;
//i=0,j的下标是从0~n-2;i=1,j的下标是从0~n-3,以此类推,发现j的下标是0~n-i-2
for(int i=0; i<n-1; i++){
for(int j=0; j<n-i-1; j++){
if(arr[j] > arr[j+1]){
temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
for(int i=0; i<n; i++){
System.out.println(arr[i]);
}
}
}
四、二维数组
int[][] arr = {{1, 2, 3}, {4, 5, 6, 7, 8, 9}};
注意:上述写法是没有错误的。这是二维数组,一维数组存放的是二维数组的地址,所以第二维度的长度是可以不相等的
二维数组定义的时候,一维必须要定义,二维可以不定义
int[][] a = new int[2][3];
或者
int[][] a = new int[2][];
五、访问修饰符
六、接口
接口其实就是一个抽象类,极度抽象的抽象类。
抽象类:一个类中一旦存在没有具体实现的抽象方法时,那么该类就必须定义为抽象类,同时抽象类允许存在非抽象方法。
但是接口完全不同,接口中不能存在非抽象方法,接口中必须全部是抽象方法。
因为接口中必须全部都是抽象方法,所以修饰抽象方法的关键字 abstract 可以省略。
接口中允许定义成员变量,但是有如下要求:
1、不能定义 private 和 protected 修饰的成员变量,只能定义public和默认访问权限修饰符修饰的成员变量。
2、接口中的成员变量在定义时就必须完成初始化。
3、接口中的成员变量都是静态常量,即可以直接通过接口访问,同时值不能被修改。
七、异常
Java 中的错误大致可以分为两类:
1、一类是编译时错误,一般是指语法错误。
2、另一类是运行时错误。
Java 中有一组专门用来描述各种不同的运行时异常,叫做异常类,Java 结合异常类提供了处理错误的机制。
具体步骤是当程序出现错误时,会创建一个包含错误信息的异常类的实例化对象,并自动将该对象提交给系统,由系统转交给能够处理异常的代码进行处理。
异常可以分为两类:
【Error 和 Exception】:
1、Error 是指系统错误,JVM 生成,我们编写的程序无法处理。
2、Exception 指程序运行期间出现的错误,我们编写的程序可以对其进行处理。
Error 和 Exception 都是 Throwable 的子类,Throwable、Error、Exception 都是存放在 java.lang 包中。
八、异常类
Java 将运行时出现的错误全部封装成类,并且不是一个类,而是一组类。同时这些类之间是有层级关系的,由树状结构一层层向下分级,处在最顶端的类是 Throwable,是所有异常类的根结点。
Error
VirtualMachineError
- StackOverflowError
- OutOfMemoryError
AWTError
IOError
Exception
IOException
- FileLockInterruptionException
- FileNotFoundException
- FileException
RuntimeException
ArithmeticException
ClassNotFoundException
IllegalArggumentException
ArrayIndexOutOfBoundsException
NullPointerException
NoSuchMethodException
NumberFormatException
九、throw和throws
throw 和 throws 是 Java 在处理异常时使用的两个关键字,都可以用来抛出异常,但是使用的方式和表示的含义完全不同。
Java 中抛出异常有 3 种方式:
try-catch
使用 throw 是开发者主动抛出异常,即读到 throw 代码就一定抛出异常,基本语法:throw new Exception(),是一种基于代码的逻辑而主动抛出异常的方式。
public class Test {
public static void main(String[] args) {
int[] array = {1,2,3};
test(array,2);
}
public static void test(int[] array,int index) {
if(index >= 3 || index < 0) {
try {
throw new Exception();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
System.out.println(array[index]);
}
}
}
- try-catch 和 throw 都是作用于具体的逻辑代码,throws 是作用于方法的,用来描述方法可能会抛出的异常。
如果方法 throws 的是 RuntimeException 异常或者其子类,外部调用时可以不处理,JVM 会处理。
如果方法 throws 的是 Exception 异常或者其子类,外部调用时必须处理,否则报错。
十、自定义异常
除了使用 Java 提供的异常外,也可以根据需求来自定义异常。
package com.southwind.exception;
public class MyNumberException extends RuntimeException {
public MyNumberException(String error) {
super(error);
}
}
package com.southwind.exception;
public class Test {
public static void main(String[] args){
Test test = new Test();
System.out.println(test.add("a"));
}
public int add(Object object){
if(object instanceof Integer) {
int num = (int)object;
return ++num;
}else {
String error = "传入的参数不是整数类型";
MyNumberException myNumberException = new MyNumberException(error);
throw myNumberException;
}
}
}
十一、多线程
1、基本概念
多线程是提升程序性能非常重要的一种方式,必须掌握的技术。使用多线程可以让程序充分利用 CPU 资源。
优点:
- 系统资源得到更合理的利用。
- 程序设计更加简洁。
- 程序响应更快,运行效率更高。
缺点:
- 需要更多的内存空间来支持多线程。
- 多线程并发访问的情况可能会影响数据的准确性。
- 数据被多线程共享,可能会出现死锁的情况。
多线程并发 -> 数据不准确 -> 线程同步 -> 死锁
2、进程和线程
什么是进程:进程就是计算机正在运行的一个独立的应用程序。进程是一个动态的概念,当我们启动某个应用的时候,进程就产生了,当我们关闭该应用的时候,进程就结束了,进程的生命周期就是我们在使用该软件的整个过程。
什么是线程?线程是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的。
应用程序是静态的,进程和线程是动态的,有创建有销毁,存在是暂时的,不是永久的。
进程和线程的区别:
进程在运行时拥有独立的内存空间,即每个进程所占用的内存空间都是独立的,互不干扰。
线程是共享内存空间的,但是每个线程的执行都是相互独立的,单独的线程是无法执行的,由进程来控制多个线程的执行。
3、多线程
多线程是指在一个进程中,多个线程同时执行,这里说的同时执行并不是真正意义的同时执行。
系统会为每个线程分配 CPU 资源,在某个具体的时间段内 CPU 资源会被一个线程占用,在不同的时间段内由不同的线程来占用 CPU 资源,所以多个线程还是在交替执行,只不过因为 CPU 运行速度太快,我们感觉是在同时执行。
public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println("李四炒菜");
}
}
}).start();
for(int i=0; i<100; i++){
System.out.println("张三炒菜");
}
}
}
执行上述代码,可能会交替输出 【张三炒菜】和【李四炒菜】。相当于main方法是主线程,然后我们又new Thread开启一个子线程。new Runnable()这种写法是匿名内部类,它有一个run方法,同时是在Test类中,属于内部类;同时它没有类名,所以称它为匿名内部类。
整个程序如果是一条回路,说明程序只有一个线程。
程序有两条回路,同时向下执行,这种情况就是多线程,两个线程同时在执行。
4、Java 中线程的使用
Java 中使用线程有两种方式:(两种基本方式,当然还有其他的实现方式)
- 继承 Thread 类
1、创建自定义类并继承Thread类
2、重写Thread类的run方法,并编写该线程的业务逻辑代码
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println("MyThread");
}
}
}
3、使用。我们定义两个线程
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println("MyThread");
}
}
}
public class MyThread2 extends Thread{
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println("MyThread2");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread2 thread2 = new MyThread2();
thread1.start();
thread2.start();
}
}
注意调用的是线程的start方法开启多线程,而不是run方法。调用start方法,MyThread和MyThread2会交替出现。而如果调用run方法,就是先执行完MyThread,再执行MyThread2,就不是多线程了。因为 run 方法调用相当于普通对象的执行,并不会去抢占 CPU 资源。只有通过start
方法才能开启线程,进而去抢占 CPU 资源,当某个线程抢占到 CPU 资源后,会自动调用 run 方法。
- 实现 Runnable 接口
1、创建自定义类并实现 Runnable 接口。
2、实现 run 方法,编写该线程的业务逻辑代码。
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<1000; i++){
System.out.println("MyRunnable");
}
}
}
3、使用
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<1000; i++){
System.out.println("MyRunnable");
}
}
}
public class MyRunnable2 implements Runnable{
@Override
public void run() {
for(int i=0; i<1000; i++){
System.out.println("MyRunnable2");
}
}
}
public class Test {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
Runnable runnable2 = new MyRunnable2();
Thread thread2 = new Thread(runnable2);
thread2.start();
}
}
线程和任务(runnable可以看作是任务):
线程是去抢占 CPU 资源的,任务是具体执行业务逻辑的,线程内部会包含一个任务,线程启动(start),当抢占到资源之后,任务就开始执行(run)。
上面的代码,我们Runnable runnable = new MyRunnable();之后,runnable.run其实也是当作普通方法去执行了,而不是多线程。由于runnable被看作是任务,它没有抢占CPU的能力,所以我们需要创建2个线程,然后将runnable塞进去,然后调用thread.start方法让线程启动,进行执行我们的runnable。
两种方式的区别:
1、MyThread,继承 Thread 类的方式,直接在类中重写 run 方法,使用的时候,直接实例化 MyThread,start 即可,因为 Thread 内部存在 Runnable。
2、MyRunnbale,实现 Runnable 接口的方法,在实现类中重写 run 方法,使用的时候,需要先创建 Thread 对象,并将 MyRunnable 注入到 Thread 中,Thread.start。
实际开发中推荐使用第二种方式。因为第二种方式把runnable单独提出来了,耦合度更低。
5、线程的状态
线程共有 5 种状态,在特定的情况下,线程可以在不同的状态之间切换,5 种状态如下所示。
创建状态:实例化一个新的线程对象,还未启动。
就绪状态:创建好的线程对象调用 start 方法完成启动,进入线程池等待抢占 CPU 资源。
运行状态:线程对象获取了 CPU 资源,在一定的时间内执行任务。
阻塞状态:正在运行的线程暂停执行任务,释放所占用的 CPU 资源,并在解除阻塞状态之后也不能直接回到运行状态,而是重新回到就绪状态,等待获取 CPU 资源。
终止状态:线程运行完毕或因为异常导致该线程终止运行。
线程状态之间的转换图。
创建状态的线程,在调用start启动线程后,线程进入就绪状态。就绪状态的线程在获取CPU资源后进入运行状态。当然线程不会一直获取到CPU资源的,当它所分配的时间片时间到期了,那么它将释放CPU资源进入就绪状态。运行状态的线程遇到线程休会进入阻塞状态(进入阻塞状态可以调用sleep、join、yield方法,sleep是休眠一段时间,join是直接抢占CPU先执行该方法,该方法执行完成其他方法才能获取到CPU,yield是在某一时刻暂停抢占CPU,在其他时刻继续抢占CPU)。阻塞状态的线程在解除阻塞后并不会直接进入运行状态,而是进入就绪状态等待CPU资源。运行状态的线程在线程执行完毕或者线程异常终止情况下,会进入终止状态。
6、线程调度
6.1、线程休眠
让当前线程暂停执行,从运行状态进入阻塞状态,将 CPU 资源让给其他线程的调度方式,通过 sleep() 来实现。
sleep是Thread类中的方法。sleep(long millis),调用时需要传入休眠时间,单位为毫秒。
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0; i<10; i++){
if(i == 5) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"MyThread");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
也可以在类的外部调用 sleep 方法。在外部调用需要注意,休眠一定要放在启动之前。
public class MyThread2 extends Thread{
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println("MyThread2");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread2 thread = new MyThread2();
try {
thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.start();
}
}
如何让主线程休眠?直接通过静态方式调用 sleep 方法。
public class Test2 {
public static void main(String[] args) {
for(int i=0; i<10; i++){
if(i == 5){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"test2");
}
}
}
public static native void sleep(long millis) throws InterruptedException;
sleep 是静态本地方法,可以通过类调用,也可以通过对象调用,方法定义抛出 InterruptedException,InterruptedException 继承 Exception,外部调用时必须手动处理异常。
异常类继承Exception则必须要手工处理异常,如果继承RuntimeException就不需要手工处理异常了,交给JVM去处理异常了。
6.2、线程合并
合并是指将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中的两个子线程顺序执行。
通过调用 join 方法来实现合并,具体如何合并?
线程甲和线程乙,线程甲执行到某个时间点的时候调用线程乙的 join方法,则表示从当前时间点开始 CPU 资源被线程乙独占,线程甲进入阻塞状态,直到线程乙执行完毕,线程甲进入就绪状态,等待获取 CPU 资源进入运行状态。
join 方法重载,join() 表示乙线程执行完毕之后才能执行其他线程,join(long millis) 表示乙线程执行 millis 毫秒之后,无论是否执行完毕,其他线程都可以和它争夺 CPU 资源。
- join()方式,乙线程执行完毕之后才能执行其他线程
public class JoinRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<20; i++){
System.out.println(i+"JoinRunnable");
}
}
}
public class JoinTest {
public static void main(String[] args) {
JoinRunnable joinRunnable = new JoinRunnable();
Thread thread = new Thread(joinRunnable);
thread.start();
for(int i=0; i<100; i++){
if(i == 10){
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"main");
}
}
}
输出结果:
0main
1main
2main
3main
4main
5main
6main
7main
8main
9main
0JoinRunnable
1JoinRunnable
2JoinRunnable
3JoinRunnable
4JoinRunnable
5JoinRunnable
6JoinRunnable
7JoinRunnable
8JoinRunnable
9JoinRunnable
10JoinRunnable
11JoinRunnable
12JoinRunnable
13JoinRunnable
14JoinRunnable
15JoinRunnable
16JoinRunnable
17JoinRunnable
18JoinRunnable
19JoinRunnable
10main
11main
12main
13main
分析:我们现在有2个线程,主线程和子线程。当主线程的i等于10的时候,将子线程加入主线程,这时子线程就会抢占CPU资源,然后先运行子线程。等子线程执行完释放CPU资源后再执行主线程。从输出上看也是9main后面都是在执行子线程,子线程执行完后再执行主线程。
- join(long millis) 表示乙线程执行 millis 毫秒之后,无论是否执行完毕,其他线程都可以和它争夺 CPU 资源
public class JoinRunnable2 implements Runnable{
@Override
public void run() {
for(int i=0; i<20; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(i+"JoinRunnable2");
}
}
}
public class JoinTest2 {
public static void main(String[] args) {
JoinRunnable2 joinRunnable2 = new JoinRunnable2();
Thread thread = new Thread(joinRunnable2);
thread.start();
for(int i=0; i<100; i++){
if(i == 10){
try {
thread.join(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"main");
}
}
}
输出结果:
0main
1main
2main
3main
4main
5main
6main
7main
8main
9main
0JoinRunnable2
1JoinRunnable2
10main
11main
12main
分析:子线程JoinRunnable2是每次休眠1秒然后输出。在主线程JoinTest2中,在i等于10的时候将JoinRunnable2加进来。在程序启动的时候,主子线程同时抢占CPU,但是子线程会休眠1秒,所以会将0main、1main、9main先打印出来;等i等于10的时候将子线程加进来,让子线程独享CPU资源3秒。注意到子线程每间隔1秒打印一次输出,所以0JoinRunnable2、1JoinRunnable2会打印出来,已经用了2秒,再等待1秒,准备打印3JoinRunnable2时,这个时候线程合并结束了。主子线程同时抢占CPU,由主线程抢到CPU,然后执行主线程逻辑。
6.3、线程礼让
线程礼让是指在某个特定的时间点,让线程暂停抢占 CPU 资源的行为,运行状态/就绪状态—》阻塞状态,将 CPU 资源让给其他线程来使用。
假如线程甲和线程乙在交替执行,某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了 CPU 资源,执行业务逻辑,但不代表线程甲一直暂停执行。线程甲只是在特定的时间节点礼让,过了时间节点,线程甲再次进入就绪状态,和线程乙争夺 CPU 资源。
通过 yield 方法实现。
public class YieldThread1 extends Thread{
@Override
public void run() {
for(int i=0; i<100; i++){
if(i == 5){
yield();
}
System.out.println(Thread.currentThread().getName()+ "----" +i);
}
}
}
public class YieldTread2 extends Thread{
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+ "----" +i);
}
}
}
public class Test {
public static void main(String[] args) {
YieldThread1 thread1 = new YieldThread1();
thread1.setName("线程1");
YieldTread2 thread2 = new YieldTread2();
thread2.setName("线程2");
thread1.start();
thread2.start();
}
}
输出结果:
线程1----0
线程1----1
线程1----2
线程1----3
线程1----4
线程2----0
线程2----1
线程2----2
线程2----3
线程1----5
线程1----6
分析:线程1在i=5的时候做一次礼让。在i取其他值的时候还是会和线程2抢CPU资源。
6.4、线程中断
有很多种情况会造成线程停止运行:
线程执行完毕自动停止
线程执行过程中遇到错误抛出异常并停止
线程执行过程中根据需求手动停止。比如网络卡顿,用户想要终止当前线程。
Java 中实现线程中断有如下几个常用方法:
- public void stop()
- public void interrupt()
- public boolean isInterrupted()
stop 方法在新版本的 JDK 已经不推荐使用,重点关注后两个方法。
interrupt 是一个实例方法,当一个线程对象调用该方法时,表示中断当前线程对象。每个线程对象都是通过一个标志位来判断当前是否为中断状态。
isInterrupted函数就是用来获取当前线程对象的标志位:
1、true 表示清除了标志位,当前线程已经中断。
2、false 表示没有清除标志位,当前对象没有中断。
当一个线程对象处于不同的状态时,中断机制也是不同的。
创建状态:实例化线程对象,不启动。
public class Test {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
}
}
输出结果:
NEW
false
NEW 表示当前线程对象为创建状态,false 表示当前线程并未中断,因为当前线程没有启动,不存在中断,不需要清除标志位。
public class Test2 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println(i+"main");
}
}
});
thread.start();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}
输出结果:
RUNNABLE
true
BLOCKED
0main
1main
2main
3main
4main
5main
6main
7main
8main
9main
6.5、设置线程优先级
一个线程的优先级越高,抢占到CPU资源的概率越大。通过setPriority(int value)来设置。
优先级的范围是1~10,默认值是5。
如下代码设置线程1、2优先级分别是1、10,这样线程2获取CPU的概率就比较大。
public class Main {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2();
myThread1.setPriority(1);
myThread2.setPriority(10);
myThread1.start();
myThread2.start();
}
}
7.1、线程同步(synchronized)
Java 中允许多线程并行访问,同一时间段内多个线程同时完成各自的操作。
多个线程同时操作同一个共享数据时,可能会导致数据不准确的问题。
public class Account implements Runnable{
private static int num;
@Override
public void run() {
try {
Thread.currentThread().sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
num++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访问");
}
}
public class Test {
public static void main(String[] args) {
Account account = new Account();
Thread t1 = new Thread(account, "张三");
Thread t2 = new Thread(account, "李四");
t1.start();
t2.start();
}
}
输出结果:输出结果不固定,可能是都是1(我感觉输出两个1可以用工作内存来解释:第一个线程要执行++,先要将值复制到工作内存当中进行加操作,准备将加操作之后的数据写入主内存的时候,CPU资源被第二个线程占用了,就出现两个1的情况了),也可能都是2.
张三是当前的第2位访问
李四是当前的第2位访问
分析:两个线程要执行的内容一样,都是给Num加1然后输出。假如线程1先抢占到CPU资源,它先运行会给Num加1,num变成1。此时准备输出Num时,CPU资源被线程2抢占,于是线程2开始执行num加1,现在num变成2了。接着线程1、2输出num就都变成2了。
使用线程同步可以解决上述问题。
可以通过 synchronized 关键字修饰方法实现线程同步,每个Java 对象都有一个内置锁,内置锁会保护使用 synchronized 关键字修饰的方法,要调用该方法就必须先获得锁,否则就处于阻塞状态。
synchronized 关键字可以修饰实例方法,也可以修饰静态方法,两者在使用的时候是有区别的。
- 静态方法加synchronized
public class SynchronizedTest {
public static void main(String[] args) {
for(int i=0; i<5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.test();
}
});
thread.start();
}
}
public synchronized static void test(){
System.out.println("start");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end");
}
}
输出结果:
start
end
start
end
start
end
start
end
start
end
因为上面调用的是static修饰的静态方法,这几个线程是共用一个静态方法,当前一个线程把该方法上锁后,其他线程只能等待该线程把锁释放了才能访问。
- 实例方法加synchronized
public class SynchronizedTest2 {
public static void main(String[] args) {
for(int i=0; i<5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest2 test2 = new SynchronizedTest2();
test2.test();
}
});
thread.start();
}
}
public synchronized void test(){
System.out.println("start");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end");
}
}
输出结果:
start
start
start
start
start
end
end
end
end
end
因为上面调用的是实例方法,每次new SynchronizedTest2();创建一个对象,然后调用test方法,调用的都是该对象本身的方法,对该对象上的锁不会影响其他对象对test方法的访问。
给实例方法(非静态方法)添加 synchronized 关键字并不能实现线程同步。
线程同步的本质是锁定多个线程所共享的资源,synchronized 还可以修饰代码块,会为代码块加上内置锁
,从而实现同步。
public class SynchronizedTest3 {
public static void main(String[] args) {
for(int i=0; i<5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest3.test();
}
});
thread.start();
}
}
public static void test() {
//给代码块加锁,括号里面是要锁定的对象,这里是锁定整个类
synchronized (SynchronizedTest3.class){
System.out.println("start");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end");
}
}
}
输出结果:
start
end
start
end
start
end
start
end
start
end
分析:synchronized (SynchronizedTest3.class)是直接给整个类加锁了,同时test是用static修饰的静态方法,5个线程公用一个方法,当1个线程上锁后,其他线程会等待。
public class SynchronizedTest3 {
public static void main(String[] args) {
for(int i=0; i<5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest3.test();
}
});
thread.start();
}
}
public static void test() {
SynchronizedTest3 test3 = new SynchronizedTest3();
//锁定test3对象
synchronized (test3){
System.out.println("start");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end");
}
}
}
输出结果:
start
start
start
start
start
end
end
end
end
end
分析:上面是在循环里面调用了5次test方法,test方法里面每执行一次就会创建一个test3对象,锁也是给test3对象加的。这个test3对象是不共享的,所以一个线程加的锁不会对其他线程有影响。
类似地,我们可以给this加锁,表示给当前对象上锁。
public class SynchronizedTest3 {
public static void main(String[] args) {
for(int i=0; i<5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest3 synchronizedTest3 = new SynchronizedTest3();
synchronizedTest3.test();
}
});
thread.start();
}
}
public void test() {
synchronized (this){
System.out.println("start");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end");
}
}
}
输出结果:
start
start
start
start
start
end
end
end
end
end
如何判断线程同步或是不同步?
找到关键点:锁定的资源在内存中是一份还是多份?一份大家需要排队,线程同步,多份(一人一份),线程不同步。
无论是锁定方法还是锁定对象,锁定类,只需要分析这个方法、对象、类在内存中有几份即可。
对象一般都是多份
类一定是一份
方法就看是静态方法还是非静态方法,静态方法一定是一份,非静态方法一般是多份.
7.2 线程通信(了解)
线程通信指的是,当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
线程通信的常见模型是:生产者与消费者模型。
使用wait、notify、notifyAll实现。
7.3 悲观锁、乐观锁
悲观锁:一上来就加锁,每次只能一个线程进入,访问完毕后再解锁。线程安全,性能较差。
乐观锁:一开始不上锁,认为是没有问题的,多个线程一起执行。等要出现线程安全问题的时候才开始控制。线程安全,性能较好。
悲观锁例子:
public class MyRunnable implements Runnable{
private int count;
@Override
public void run() {
for(int i=0; i<100; i++){
synchronized (this) {
System.out.println("count =====>" + (++count));
}
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
for(int i=0; i<100; i++){
new Thread(runnable).start();
}
}
}
上面代码执行最终输出10000。
如果我们不加锁呢,多运行几次发现,结果不等于10000的情况很少,也就是说出现线程不安全的概率很低,所以索性不加锁,因为加锁会损耗性能。
乐观锁:
用到了CAS算法(比较和修改)。
多个线程同时操作共享变量。假设这个变量的值为10,乐观锁是不会加锁的。当一个线程要修改共享变量的时候,它会把这个变量的初始值10留一个备份,然后对这个变量进行加1操作变成11。然后拿初始值的备份与当前值进行比较,如果相等证明这个变量没有被修改,于是把11放心地写入变量;如果比较的时候发现初始值备份与当前值不相等,说明这个变量的值被其他线程修改过,于是本次加1变为11的操作作废,重新计算一次加1。
public class MyRunnable2 implements Runnable{
//整数修改的乐观锁,原子类实现的
private AtomicInteger count = new AtomicInteger();
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println("count =====>" + count.incrementAndGet());
}
}
}
上面代码运行输出结果是10000。
8、线程安全的单例模式
单例模式是一种常见的软件设计模式,核心思想是一个类只有一个实例对象。
JVM:栈内存、堆内存
单线程模式下的单例模式
package com.southwind;
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
System.out.println("创建了SingletonDemo对象");
}
public static SingletonDemo getInstance(){
if(singletonDemo == null){
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}
}
上述代码在单线程下是单例模式,在多线程下就不是单例模式了。
package com.southwind;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo singletonDemo = SingletonDemo.getInstance();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo singletonDemo = SingletonDemo.getInstance();
}
}).start();
}
}
上述代码可能会输出两次创建了SingletonDemo对象。因为线程1在判断singletonDemo == null时,准备在new SingletonDemo时,可能会被线程2抢占CPU资源。线程2判断singletonDemo == null为true,也会new SingletonDemo。于是调用两次构造函数,出现两个对象了。解决方法就是getInstance方法使用synchronized
多线程模式下的单例模式
package com.southwind;
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
System.out.println("创建了SingletonDemo对象");
}
public synchronized static SingletonDemo getInstance(){
if(singletonDemo == null){
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}
}
但是上面的代码还是不够好。假如getInstance方法不光是创建singleton对象,它的后面还有一些业务逻辑。这样写就会导致整个方法锁起来,其他线程必须等待当前线程业务全部执行完成后才能执行,效率就比较低。所以就不应该锁方法,而是将synchronized移到代码块处。
双重检测,synchronized 修饰代码块。
1、线程同步是为了实现线程安全,如果只创建一个对象,那么线程就是安全的。
2、如果 synchronized 锁定的是多个线程共享的数据(同一个对象),那么线程就是安全的。
package com.southwind;
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
System.out.println("创建了SingletonDemo对象");
}
public static SingletonDemo getInstance(){
if(singletonDemo == null){
synchronized(SingletonDemo.class){
if(singletonDemo == null){
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
}
上述的代码还不完美。
最终的单例模式代码。加了volatile关键字
package com.southwind;
public class SingletonDemo {
private volatile static SingletonDemo singletonDemo;
private SingletonDemo(){
System.out.println("创建了SingletonDemo对象");
}
public static SingletonDemo getInstance(Integer i){
if(singletonDemo == null){
synchronized(i){
if(singletonDemo == null){
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
}
volatile 的作用是可以使内存中的数据对线程可见。
主内存对线程是不可见的,添加 volatile 关键字之后,主内存对线程可见。
java内存
主内存对线程是不可见的。线程1要操作主内存中的num,真正操作的是从主内存复制到工作内存中的一个副本。对副本进行读写操作之后,然后将工作内存的数据写入主内存。
上述代码如果不加volatile,线程1读取从主内存中复制到工作内存中的singletonDemo,发现是null,于是new SingletonDemo。因为锁只是加在了new SingletonDemo这个步骤了,做完这一步锁就释放了。线程1还需要将工作内存中new出来的对象写入主内存。还没将new对象写入主内存,线程2就开始执行了,读取从主内存复制到工作内存中的副本singletonDemo,发现是null,也会new对象。这样又有两个对象了。
加了volatile后,主内存对线程可见,就会直接在主内存new对象,释放锁之后,线程2看到主内存已经有对象了,就不会再次new对象了。
注意:以下代码是线程同步的。synchronized锁的是i,i是整形的,并且传入的值一样,就认为是同一个资源。(两个Integer对象所指向的内存地址相同,指向同一块区域)
1
package com.southwind;
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
System.out.println("创建了SingletonDemo对象");
}
public static SingletonDemo getInstance(Integer i){
if(singletonDemo == null){
synchronized(i){
if(singletonDemo == null){
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
}
package com.southwind;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Integer a = Integer.parseInt("1");
SingletonDemo singletonDemo = SingletonDemo.getInstance(a);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Integer b = Integer.parseInt("1");
SingletonDemo singletonDemo = SingletonDemo.getInstance(b);
}
}).start();
}
}
注意:以下代码不是线程同步的。synchronized锁的是i,i是整形的,并且传入的值不一样,就认为是不同的资源。
package com.southwind;
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
System.out.println("创建了SingletonDemo对象");
}
public static SingletonDemo getInstance(Integer i){
if(singletonDemo == null){
synchronized(i){
if(singletonDemo == null){
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
}
package com.southwind;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Integer a = Integer.parseInt("1");
SingletonDemo singletonDemo = SingletonDemo.getInstance(a);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Integer b = Integer.parseInt("2");
SingletonDemo singletonDemo = SingletonDemo.getInstance(b);
}
}).start();
}
}
9、死锁 DeadLock
前提:一个线程完成业务需要同时访问两个资源。
死锁:多个线程同时在完成业务,出现争抢资源的情况。
资源类
package com.southwind.demo1;
public class DeadLockRunnable implements Runnable{
//人的编号
public int num;
//筷子(资源)
private static Chopsticks chopsticks1 = new Chopsticks();
private static Chopsticks chopsticks2 = new Chopsticks();
/**
* num=1,拿到chopsticks1,等待chopsticks2
* num=2,拿到chopsticks2,等待chopsticks1
*/
@Override
public void run() {
if(num == 1){
System.out.println(Thread.currentThread().getName()+"拿到了chopsticks1,请求chopsticks2");
//拿到筷子1的实现方式:只需要给筷子1上锁就行
synchronized(chopsticks1){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//请求chopsticks2,也是给筷子2上锁就行
synchronized (chopsticks2){
System.out.println(Thread.currentThread().getName()+"开始吃饭");
}
}
}
if(num == 2){
System.out.println(Thread.currentThread().getName()+"拿到了chopsticks2,请求chopsticks1");
synchronized(chopsticks2){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (chopsticks1){
System.out.println(Thread.currentThread().getName()+"开始吃饭");
}
}
}
}
}
package com.southwind.demo1;
public class DeadLockTest {
public static void main(String[] args) {
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1, "张三").start();
new Thread(deadLockRunnable2, "李四").start();
}
}
输出结果:
张三拿到了chopsticks1,请求chopsticks2
李四拿到了chopsticks2,请求chopsticks1
上述代码输出,张三和李四各拿到一根筷子,程序出现死锁,一直不会结束。
如何破解死锁
不要让多线程并发访问
package com.southwind.demo1;
public class DeadLockTest {
public static void main(String[] args) {
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1, "张三").start();
//主线程休眠,保证线程1执行完成
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(deadLockRunnable2, "李四").start();
}
}
10、lambda 表达式优化代码
package com.southwind.demo1;
public class Test3 {
public static void main(String[] args) {
//lambda表达式
new Thread(() -> {
for(int i=0; i<10; i++){
System.out.println("test111");
}
}).start();
}
}
上述代码将new Runnable的代码替换为lambda表达式。lambda表达式的格式:() -> {}。圆括号里面写要传的参数,花括号里面对象的是业务逻辑代码,对应的就是runnable的run方法。
注意:可以替换为lambda表达式写法的,接口必须是抽象式接口,即用@FunctionalInterface注解修饰。例如上面被替换的runnable就是函数式接口
11、Lock、ReentrantLock
synchronized是实现线程同步的初级方法。Lock是实现线程同步更为高阶的方法。
java.util.concurrent(JUC)并发编程常用的包。
Lock 是一个接口,用来实现线程同步的,功能与 synchronized 一样。
Lock 使用频率最高的实现类是 ReentrantLock(重入锁,即可以多次上锁),可以重复上锁。
使用TimeUnit实现休眠
package com.southwind.demo2;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
public class Test {
public static void main(String[] args) {
System.out.println(1);
try {
//休眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(2);
}
}
还是统计访问量,使用synchronized上锁
package com.southwind.demo2;
public class Test {
public static void main(String[] args) {
Account account = new Account();
new Thread(account, "A").start();
new Thread(account, "B").start();
}
}
class Account implements Runnable{
private static int num;
@Override
public synchronized void run() {
num++;
System.out.println(Thread.currentThread().getName()+"-" + num);
}
}
使用reentrantLock上锁
package com.southwind.demo2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
Account account = new Account();
new Thread(account, "A").start();
new Thread(account, "B").start();
}
}
class Account implements Runnable{
private static int num;
//定义锁
private Lock lock = new ReentrantLock();
@Override
public void run() {
//上锁,重复上锁
lock.lock();
lock.lock();
num++;
System.out.println(Thread.currentThread().getName()+"-" + num);
//解锁
lock.unlock();
lock.unlock();
}
}
JUC:java.util.concurrent
Java 并发编程工具包,Java 官方提供的一套专门用来处理并发编程的工具集合(接口+类)
并发:单核 CPU,多个线程“同时”运行,实际是交替执行,只不过速度太快,看起来是同时执行。两个厨师一口锅。
并行:多核 CPU,真正的多个线程同时运行。两个厨师两口锅。
重入锁是 JUC 使用频率非常高的一个类 ReentrantLock
ReentrantLock 就是对 synchronized 的升级,目的也是为了实现线程同步。
- ReentrantLock 是一个类,synchronized 是一个关键字。
- ReentrantLock 是 JDK 实现,synchronized 是 JVM 实现。
- synchronized 自动上锁,自动释放锁,ReentrantLock 手动上锁,手动释放锁。
- synchronized 是非公平锁,Lock 可以设置是否为公平锁。
- synchronized 无法判断是否获取到了锁,Lock 可以判断是否拿到了锁。
- synchronized 拿不到锁就会一直等待,Lock 不一定会一直等待。
ReentrantLock 是 Lock 接口的实现类。默认是非公平锁。
公平锁和非公平锁的区别
公平锁:线程同步时,多个线程排队,依次执行
非公平锁:线程同步时,可以插队
中断
ReentrantLock 除了可以重入之外,还有一个可以中断的特点,可中断是指某个线程在等待获取锁的过程中可以主动过终止线程。
synchronized无法实现一些场景,比如我们要下载一个资源,但是下载过程中卡住了。这就会导致这个线程一直处于阻塞状态,无法主动释放锁。ReentrantLock就可以优化这些场景。
package com.southwind.demo2;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
StopLock stopLock = new StopLock();
Thread t1 = new Thread(()->{
stopLock.service();
},"A");
Thread t2 =new Thread(()->{
stopLock.service();
},"B");
t1.start();
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
//中断线程2
t2.interrupt();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class StopLock{
private ReentrantLock reentrantLock = new ReentrantLock();
public void service() {
try {
//声明该锁可以被中断
reentrantLock.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"get lock");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
12、生产者消费者模式
在一个生产环境中,生产者和消费者在同一时间段内共享同一块缓冲区,生产者负责向缓冲区添加数据,消费者负责从缓冲区取出数据。
1、汉堡类
package com.southwind.demo3;
public class Hamburger {
//id用来区分每个汉堡
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Hamburger{" +
"id=" + id +
'}';
}
public Hamburger(int id) {
this.id = id;
}
}
2、容器类
package com.southwind.demo3;
public class Container {
//定义一个容量为6的数组来存储汉堡
public Hamburger[] arrays = new Hamburger[6];
//要取出的汉堡
public int index = 0;
/**
* 向容器中添加汉堡
*/
public synchronized void push(Hamburger hamburger){
//当index等于数组长度,表明容器满了,需要让生产者等待
while(index == arrays.length){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//当index不等于数组长度,表明容器没有满,需要让生产者开始生产
this.notify();
arrays[index] = hamburger;
index++;
System.out.println("生成了一个汉堡"+hamburger);
}
/**
* 从容器中取出汉堡
*/
public synchronized Hamburger pop(){
//如果index等于0,表明没有汉堡了就让当前线程等待
while (index == 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//当index不等于0,表明有汉堡了就唤醒当前线程
this.notify();
index--;
System.out.println("消费了一个汉堡"+arrays[index]);
return arrays[index];
}
}
3、生产者
package com.southwind.demo3;
import java.util.concurrent.TimeUnit;
public class Producer {
//定义容器来存汉堡
private Container container;
public Producer(Container container){
this.container = container;
}
public void product(){
for(int i=0; i<30; i++){
//生产汉堡并将汉堡装入容器
Hamburger hamburger = new Hamburger(i);
this.container.push(hamburger);
//生产完后休眠1秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
4、消费者
package com.southwind.demo3;
import java.util.concurrent.TimeUnit;
public class Consumer {
private Container container;
public Consumer(Container container) {
this.container = container;
}
public void consume(){
for(int i=0 ;i<30; i++){
this.container.pop();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
5、测试类
package com.southwind.demo3;
public class Test {
public static void main(String[] args) {
Container container = new Container();
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
new Thread(()->{
producer.product();
}).start();
new Thread(()->{
consumer.consume();
}).start();
}
}
输出结果:
生成了一个汉堡Hamburger{id=0}
消费了一个汉堡Hamburger{id=0}
生成了一个汉堡Hamburger{id=1}
消费了一个汉堡Hamburger{id=1}
生成了一个汉堡Hamburger{id=2}
消费了一个汉堡Hamburger{id=2}
生成了一个汉堡Hamburger{id=3}
消费了一个汉堡Hamburger{id=3}
生成了一个汉堡Hamburger{id=4}
消费了一个汉堡Hamburger{id=4}
生成了一个汉堡Hamburger{id=5}
消费了一个汉堡Hamburger{id=5}
13、多线程并发卖票
package com.southwind.demo4;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket {
//剩余球票
private int remainCount = 15;
//已卖出球票
private int outCount = 0;
private Lock lock = new ReentrantLock();
public void sale(){
while(remainCount > 0){
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(remainCount == 0) return;
lock.lock();
//卖票
remainCount--;
outCount++;
if(remainCount == 0){
System.out.println(Thread.currentThread().getName()+"已卖出第"+outCount+"张票,票已卖光");
}else{
System.out.println(Thread.currentThread().getName()+"已卖出第"+outCount+"张票,剩余"+remainCount+"张票");
}
lock.unlock();
}
}
}
package com.southwind.demo4;
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
ticket.sale();
}, "A").start();
new Thread(()->{
ticket.sale();
}, "B").start();
new Thread(()->{
ticket.sale();
}, "C").start();
}
}
输出结果:
A已卖出第1张票,剩余14张票
C已卖出第2张票,剩余13张票
B已卖出第3张票,剩余12张票
B已卖出第4张票,剩余11张票
A已卖出第5张票,剩余10张票
C已卖出第6张票,剩余9张票
B已卖出第7张票,剩余8张票
A已卖出第8张票,剩余7张票
C已卖出第9张票,剩余6张票
C已卖出第10张票,剩余5张票
A已卖出第11张票,剩余4张票
B已卖出第12张票,剩余3张票
C已卖出第13张票,剩余2张票
B已卖出第14张票,剩余1张票
A已卖出第15张票,票已卖光
十二、Java 并发编程
1、互联网分布式架构设计,提高系统并发能力的方式:
- 垂直扩展
- 水平扩展
垂直扩展
提升单机处理能力
1、提升单机的硬件设备,增加 CPU 核数,升级网卡,硬盘扩容,升级内存。
2、提升单机的架构性能,使用 Cache 提高效率,使用异步请求来增加单服务吞吐量,NoSQL 提升数据库访问能力。
水平扩展
集群:一个厨师搞不定,多雇几个厨师一起炒菜,多个人干同一件事情。
分布式:给厨师雇两个助手,一个负责洗菜,一个负责切菜,厨师只负责炒菜,一件事情拆分成多个步骤,由不同的人去完成。
站点层扩展:Nginx 反向代理,一个 Tomcat 跑不动,那就 10 个 Tomcat 去跑。
服务层扩展:RPC 框架实现远程调用,Spring Boot/Spring Cloud,Dubbo,分布式架构,将业务逻辑拆分到不同的 RPC Client,各自完成对应的业务,如果某项业务并发量很大,增加新的 RPC Client,就能扩展服务层的性能,做到理论上的无限高并发。
数据层扩展:在数据量很大的情况下,将原来的一台数据库服务器,拆分成多台,以达到扩充系统性能的目的,主从复制,读写分离,分表分库。
2、Java 默认(随便写一个java程序)的线程数 2 个
- main 主线程
- GC 垃圾回收机制
Java 本身是无法开启线程的,因为Java 无法操作硬件,只能通过调用本地方法,C++ 编写的动态函数库。
3、Java 中实现多线程有几种方式?
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
Callable 和 Runnable 的区别在于 Runnable 的 run 方法没有返回值,Callable 的 call 方法有返回值。
package com.southwind;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
//callable定义是string类型,FutureTask也对应定义成string
FutureTask<String> futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
//获取callable的call方法的返回值
try {
String value = futureTask.get();
System.out.println(value);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("callable");
return "hello";
}
}
输出结果:
callable
hello
Callable不能直接往thread里面传。thread是要传入runnable对象,所以我们需要一次转换。futureTask是runnable的实现类,所以我们把callable转换为futureTask就可以,然后传入thread。
4、sleep 和 wait
sleep 是Thread类中的方法,是让当前线程休眠;wait 是Object类的方法,是让访问当前对象的线程休眠。
sleep 不会释放锁,wait 会释放锁。
5、synchronized 锁定的是什么
1、synchronized 修饰非静态方法,锁定方法的调用者
package com.southwind;
import org.omg.CORBA.TIMEOUT;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
data.func1();
}, "A").start();
new Thread(()->{
data.func2();
}, "B").start();
}
}
class Data{
public synchronized void func1(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1....");
}
public synchronized void func2(){
System.out.println("2....");
}
}
输出结果:
1....
2....
分析:上面的代码synchronized是修改非静态方法,锁定的是方法调用者,即data。由于data是共享数据,先是由线程A抢占CPU执行。线程A会对data上锁,线程B就会等待锁的释放。查看输出结果会看到先等待3秒,然后输出1,释放锁;然后线程B开始执行并输出2
package com.southwind;
import org.omg.CORBA.TIMEOUT;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
Data data = new Data();
Data data2 = new Data();
new Thread(()->{
data.func1();
}, "A").start();
new Thread(()->{
data2.func2();
}, "B").start();
}
}
class Data{
public synchronized void func1(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1....");
}
public synchronized void func2(){
System.out.println("2....");
}
}
输出结果:
2....
1....
分析:上面的代码是加了一个data2。data.func1()锁定的是data对象,data2.func2()锁定的是data2对象。这两个是不同的对象,所以对data上锁不会影响对data2的访问。看结果是先输出2,等待3秒后输出1
package com.southwind;
import org.omg.CORBA.TIMEOUT;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
data.func1();
}, "A").start();
new Thread(()->{
data.func3();
}, "B").start();
}
}
class Data{
public synchronized void func1(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1....");
}
public synchronized void func2(){
System.out.println("2....");
}
public void func3(){
System.out.println("3....");
}
}
输出结果:
3....
1....
分析:我们新加了一个没加synchronized关键字修改的方法func3,这个方法没上锁,所以执行不受影响。先输出3,等待3秒输出1
2、synchronized 修饰静态方法,锁定的是类
package com.southwind;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
data.func1();
}, "A").start();
new Thread(()->{
data.func2();
}, "B").start();
}
}
class Data{
public synchronized static void func1(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1....");
}
public synchronized static void func2(){
System.out.println("2....");
}
}
输出结果:
1....
2....
分析:使用synchronized static修改方法其实锁定的是这个类,不管调用者是哪个对象。线程A先执行,所以看结果是先等待3秒,接着输出1、2
package com.southwind;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
Data data = new Data();
Data data2 = new Data();
new Thread(()->{
data.func1();
}, "A").start();
new Thread(()->{
data2.func2();
}, "B").start();
}
}
class Data{
public synchronized static void func1(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1....");
}
public synchronized static void func2(){
System.out.println("2....");
}
}
输出结果:
1....
2....
分析:使用synchronized static修改方法其实锁定的是这个类,不管调用者是哪个对象。线程A先执行,所以看结果是先等待3秒,接着输出1、2
3、synchronized 静态方法和实例方法同时存在,静态方法锁定的是类,实例方法锁定的是对象
package com.southwind;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
data.func1();
}, "A").start();
new Thread(()->{
data.func2();
}, "B").start();
}
}
class Data{
public synchronized static void func1(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1....");
}
public synchronized void func2(){
System.out.println("2....");
}
}
输出结果:
2....
1....
分析:synchronized锁定静态方法最终锁定的是类,锁定普通方法最终锁定的是对象。这两个是不同的。所以上面是先输出2,等待3秒输出1
6、Lock
tryLock
package com.southwind;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
new Thread(()->{
timeLock.getLock();
}, "A").start();
new Thread(()->{
timeLock.getLock();
}, "B").start();
}
}
class TimeLock{
private Lock lock = new ReentrantLock();
public void getLock(){
try {
//如果3秒内拿到锁返回true,否则返回false
if(lock.tryLock(3, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"拿到了锁");
TimeUnit.SECONDS.sleep(5);
}else{
System.out.println(Thread.currentThread().getName()+"没拿到锁");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//解锁
lock.unlock();
}
}
}
输出结果:
分析:定义A、B两个线程,同时去调用getLock方法。这里是B先拿到锁,然后休眠了5秒。休眠过程中释放CPU资源,A执行getLock方法,尝试3秒内获取锁,此时锁是B拿到了,B休眠5秒,A在3秒内是无法获取到锁的,所以输出了A没拿到锁。最后A去执行lock.unlock方法去解锁,抛出异常。因为锁是B上的,A没有办法去解锁。
针对上面抛异常,我们可以这样优化代码
package com.southwind;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
new Thread(()->{
timeLock.getLock();
}, "A").start();
new Thread(()->{
timeLock.getLock();
}, "B").start();
}
}
class TimeLock{
private ReentrantLock lock = new ReentrantLock();
public void getLock(){
try {
//如果3秒内拿到锁返回true,否则返回false
if(lock.tryLock(3, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"拿到了锁");
TimeUnit.SECONDS.sleep(5);
}else{
System.out.println(Thread.currentThread().getName()+"没拿到锁");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//解锁。isHeldByCurrentThread表示当前的锁是否被当前线程占用
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
输出结果:
A拿到了锁
B没拿到锁
7、生产者消费者问题
synchronized
package com.southwind;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for(int i=0; i<30; i++){
data.increment();
}
}, "A").start();
new Thread(()->{
for(int i=0; i<30; i++) {
data.decrement();
}
}, "B").start();
}
}
class Data{
private int num = 0;
public synchronized void increment(){
while(num != 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.notify();
num++;
System.out.println("生产了一个汉堡"+num);
}
public synchronized void decrement(){
while(num == 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.notify();
num--;
System.out.println("消费了一个汉堡"+num);
}
}
必须使用 while 判断,不能用 if,因为 if 会存在线程虚假唤醒的问题,虚假唤醒就是一些 wait 方法会在除了 notify 的其他情况被唤醒,不是真正的唤醒,使用 while 完成多重检测,避免这一问题。
lock
package com.southwind;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for(int i=0; i<30; i++){
data.increment();
}
}, "A").start();
new Thread(()->{
for(int i=0; i<30; i++) {
data.decrement();
}
}, "B").start();
}
}
class Data{
private int num = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment(){
lock.lock();
while(num != 0){
try {
condition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
condition.signal();
num++;
System.out.println("生产了一个汉堡"+num);
lock.unlock();
}
public void decrement(){
lock.lock();
while(num == 0){
try {
condition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
condition.signal();
num--;
System.out.println("消费了一个汉堡"+num);
lock.unlock();
}
}
使用 Lock 锁,就不能通过 wait 和 notify 来暂停线程和唤醒线程,而应该使用 Condition 的 await和 signal来暂停和唤醒线程。
8、ConcurrentModificationException
并发访问异常
package com.southwind;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for(int i=0; i<10;i++){
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//写
list.add("a");
//读
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
执行上述代码,可能会出现如下异常:这是因为多个线程同时对list进行读写操作。
如何解决**
1、换用vector
public class Test {
public static void main(String[] args) {
List<String> list = new Vector<>();
for(int i=0; i<10;i++){
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//写
list.add("a");
//读
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
vector的add方法使用synchronized修饰,是线程安全的。
arrayList的add方法未使用synchronized修饰,不是线程安全的。
2、Collections.synchronizedList
3、JUC:CopyOnWriteArrayList
public class Test {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for(int i=0; i<10;i++){
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//写
list.add("a");
//读
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
CopyOnWrite 写时复制,当我们往一个容器添加元素的时候,不是直接给容器添加,而是先将当前容器复制一份,向新的容器中添加数据,添加完成之后,再将原容器的引用指向新的容器。
9、读写锁
接口 ReadWriteLock,实现类是 ReentrantReadWriteLock,可以多线程同时读,但是同一时间内只能有一个线程进行写入操作。
读写锁也是为了实现线程同步,只不过粒度更细,可以分别给读和写的操作设置不同的锁。
package com.southwind;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.*;
public class Test {
public static void main(String[] args) {
Cache cache = new Cache();
for(int i=0; i<5; i++){
final int temp = i;
new Thread(()->{
cache.write(temp, String.valueOf(temp));
}).start();
}
for(int i=0; i<5; i++){
final int temp = i;
new Thread(()->{
cache.read(temp);
}).start();
}
}
}
class Cache{
private Map<Integer, String> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 写操作
* @param key
* @param value
*/
public void write(Integer key, String value){
readWriteLock.writeLock().lock();
System.out.println(key+"开始写入");
map.put(key, value);
System.out.println(key+"写入完毕");
readWriteLock.writeLock().unlock();
}
/**
* 读操作
* @param key
*/
public void read(Integer key){
readWriteLock.readLock().lock();
System.out.println(key+"开始读取");
map.get(key);
System.out.println(key+"读取完毕");
readWriteLock.readLock().unlock();
}
}
使用读写锁,可以保证写操作过程中没有读操作的干扰。读操作可以同时进行。
写入锁也叫独占锁,只能被一个线程占用,读取锁也叫共享锁,多个线程可以同时占用。
10、线程池
预先创建好一定数量的线程对象,存入缓冲池中,需要用的时候直接从缓冲池中取出,用完之后不要销毁,还回到缓冲池中,为了提高资源的利用率。
优势:
- 提高线程的利用率
- 提高响应速度
- 便于统一管理线程对象
- 可以控制最大的并发数(通过设置线程池的maxsize)
1、线程池初始化的时候创建一定数量的线程对象。
2、如果缓冲池中没有空闲的线程对象,则新来的任务进入等待队列。
3、如果缓冲池中没有空闲的线程对象,等待队列也已经填满,可以申请再创建一定数量的新线程对象,直到到达线程池的最大值,这时候如果还有新的任务进来,只能选择拒绝。
使用工具类创建线程池,不推荐
- 单例模式的线程池。线程池中只有一个线程
package com.southwind;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
//单例
ExecutorService executorService = Executors.newSingleThreadExecutor();
for(int i=0; i<10; i++){
final int temp = i;
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"---"+temp);
});
}
//关闭线程池
executorService.shutdown();
}
}
输出结果:
pool-1-thread-1---0
pool-1-thread-1---1
pool-1-thread-1---2
pool-1-thread-1---3
pool-1-thread-1---4
pool-1-thread-1---5
pool-1-thread-1---6
pool-1-thread-1---7
pool-1-thread-1---8
pool-1-thread-1---9
分析:上面的代码输出的线程名都是pool-1-thread-1,属于单例模式
- 指定线程池中线程数量
package com.southwind;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
//执行线程池线程数量
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i=0; i<10; i++){
final int temp = i;
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"---"+temp);
});
}
//关闭线程池
executorService.shutdown();
}
}
输出结果:
pool-1-thread-1---0
pool-1-thread-4---3
pool-1-thread-3---2
pool-1-thread-4---6
pool-1-thread-4---8
pool-1-thread-4---9
pool-1-thread-2---1
pool-1-thread-3---7
pool-1-thread-5---4
pool-1-thread-1---5
分析:上面输出的线程名有5个线程,每个线程都执行了任务
- 缓存线程池,线程池中线程数量由电脑配置决定,配置越高线程越多(数量是随机的)
package com.southwind;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
//缓存线程池,线程池中线程数量由电脑配置决定,配置越高线程越多(数量是随机的)
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0; i<10; i++){
final int temp = i;
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"---"+temp);
});
}
//关闭线程池
executorService.shutdown();
}
}
输出结果:
pool-1-thread-1---0
pool-1-thread-4---3
pool-1-thread-3---2
pool-1-thread-2---1
pool-1-thread-5---4
pool-1-thread-6---5
pool-1-thread-7---6
pool-1-thread-8---7
pool-1-thread-8---9
pool-1-thread-4---8
上面三种方法,无论哪种线程池,都是工具类 Executors 封装的,底层代码都一样,都是通过创建 ThreadPoolExecutor 对象来完成线程池的构建。
ThreadPoolExecutor 底层代码
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:核心池大小(线程池中线程数量下限),初始化的线程数量
maximumPoolSize:线程池最大线程数,它决定了线程池容量的上限
corePoolSize 就是线程池的大小,maximumPoolSize 是一种补救措施,任务量突然增大的时候的一种补救措施。
keepAliveTime:线程对象的存活时间(在没有任务可执行的情况下),必须是线程池中的数量大于 corePoolSize,才会生效(在没有任务可执行的情况下,多开的线程可以释放了)
unit:线程对象存活时间单位
workQueue:等待队列,一个阻塞队列,用来存储等待执行的任务,常用的阻塞队列有以下几种:
- ArrayBlockingQueue:基于数组的先进先出队列,创建时必须指定大小。
- LinkedBlockingQueue:基于链表的先进先出队列,创建时可以不指定大小,默认值时 Integer.MAX_VALUE。
- SynchronousQueue:它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。
- PriorityBlockingQueue:具有优先级的阻塞队列。
threadFactory:线程工厂,用来创建线程对象
RejectedExecutionHandler:拒绝策略(线程被占满后会触发)
- AbortPolicy:直接抛出异常
- DiscardPolicy:放弃任务,不抛出异常
- DiscardOldestPolicy:尝试与等待队列中最前面的任务去争夺,不抛出异常
- CallerRunsPolicy:谁调用谁处理
使用工具类创建线程池的底层源码
1、单例
Executors.newSingleThreadExecutor();
单例场景下,核心池和最大线程数量都是1,保证只有一个线程对象。keepAliveTime为0,等待队列为LinkedBlockingQueue链表结构。然后调用ThreadPoolExecutor方法。
ThreadPoolExecutor使用单例场景传入的前5个参数,第6个参数是默认的线程工厂。
第7个参数是默认的拒绝策略AbortPolicy:直接抛出异常
2、指定线程池线程数量
Executors.newFixedThreadPool(5);
核心池和最大线程数量都是通过参数指定,其他参数和单例是一样的。
3、缓存线程池
核心池为0(电脑配置比较垃圾,运行不起来线程),最大线程数量为Integer类型的最大值。线程存活时间为60秒。
拒绝策略是默认的AbortPolicy:直接抛出异常
4、自定义线程池(推荐这种方式)
package com.southwind;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService executorService = null;
try {
//自定义线程池,核心池大小为2,最大线程数量为3,线程存活1秒,等待队列大小为2,采用默认拒绝策略
executorService = new ThreadPoolExecutor(
2,
3,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//执行业务逻辑
for(int i=0;i<1;i++){
executorService.execute(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"正在办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭线程池
executorService.shutdown();
}
}
}
输出结果:
pool-1-thread-1正在办理业务
分析:我们的核心池有2个线程,来了1个任务是足够处理的。
我们把任务数变为3
package com.southwind;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService executorService = null;
try {
//自定义线程池,核心池大小为2,最大线程数量为3,线程存活1秒,等待队列大小为2,采用默认拒绝策略
executorService = new ThreadPoolExecutor(
2,
3,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//执行业务逻辑
for(int i=0;i<3;i++){
executorService.execute(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"正在办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭线程池
executorService.shutdown();
}
}
}
输出结果:
pool-1-thread-1正在办理业务
pool-1-thread-2正在办理业务
pool-1-thread-1正在办理业务
分析:核心池2个线程,前2个任务可以处理,第3个任务进入等待队列,前2个线程空闲下来就可以处理第3个任务了。
我们把任务数变为5
package com.southwind;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService executorService = null;
try {
//自定义线程池,核心池大小为2,最大线程数量为3,线程存活1秒,等待队列大小为2,采用默认拒绝策略
executorService = new ThreadPoolExecutor(
2,
3,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//执行业务逻辑
for(int i=0;i<5;i++){
executorService.execute(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"正在办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭线程池
executorService.shutdown();
}
}
}
输出结果:
pool-1-thread-3正在办理业务
pool-1-thread-1正在办理业务
pool-1-thread-2正在办理业务
pool-1-thread-3正在办理业务
pool-1-thread-1正在办理业务
分析:5个任务,其中2个任务由核心池中的2个线程处理,其中2个任务进入等待队列,发现还有一个线程没处理。于是新加线程,线程池中线程数量变为3,这时就可以处理了。
我们把任务数变为6
package com.southwind;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService executorService = null;
try {
//自定义线程池,核心池大小为2,最大线程数量为3,线程存活1秒,等待队列大小为2,采用默认拒绝策略
executorService = new ThreadPoolExecutor(
2,
3,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//执行业务逻辑
for(int i=0;i<6;i++){
executorService.execute(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"正在办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭线程池
executorService.shutdown();
}
}
}
输出结果:
java.util.concurrent.RejectedExecutionException: Task com.southwind.Test$$Lambda$1/1096979270@448139f0 rejected from java.util.concurrent.ThreadPoolExecutor@7cca494b[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.southwind.Test.main(Test.java:21)
pool-1-thread-1正在办理业务
pool-1-thread-2正在办理业务
pool-1-thread-3正在办理业务
pool-1-thread-1正在办理业务
pool-1-thread-2正在办理业务
分析:6个任务,此时线程池中线程数量为3,只能处理3个任务,等待队列大小为2,剩下一个线程不能处理,就会触发拒绝策略,这里我们定义的是AboutPolicy–抛出异常
不同的拒绝策略
- AbortPolicy()
- CallerRunsPolicy() 线程池最大大小为5,来了6个任务,采用这种策略谁调用谁处理,第6个任务就交给了主线程处理了。
- DiscardPolicy(),直接拒绝,不会抛出异常。6个任务只执行了5个,第6个被拒绝
- DiscardOldestPolicy(),尝试与等待队列中最老的那一个任务竞争,竞争过就执行。竞争不过就放弃,不会抛出异常。
线程池 3 大考点:
1、Executors 工具类的 3 种实现
ExecutorService executorService = Executors.newSingleThreadExecutor();
ExecutorService executorService = Executors.newFixedThreadPool(5);
ExecutorService executorService = Executors.newCachedThreadPool();
2、7 个参数
corePoolSize:核心池的大小
maximumPoolSize:线程池的最大容量
keepAliveTime:线程存活时间(在没有任务可执行的情况下),必须是线程池中的数量大于 corePoolSize,才会生效
TimeUnit:存活时间单位
BlockingQueue:等待队列,存储等待执行的任务
ThreadFactory:线程工厂,用来创建线程对象
RejectedExecutionHandler:拒绝策略
3、4 种拒绝策略
1、AbortPolicy:直接抛出异常
2、DiscardPolicy:放弃任务,不抛出异常
3、DiscardOldestPolicy:尝试与等待队列中最前面的任务去争夺,不抛出异常
4、CallerRunsPolicy:谁调用谁处
11、ForkJoin 框架
ForkJoin 是 JDK 1.7 后发布的多线程并发处理框架,功能上和 JUC 类似,JUC 更多时候是使用单个类完成操作,ForkJoin 使用多个类同时完成某项工作,处理上比 JUC 更加丰富,实际开发中使用的场景并不是很多,互联网公司真正有高并发需求的情况才会使用,面试时候会加分
本质上是对线程池的一种的补充,对线程池功能的一种扩展,基于线程池的,它的核心思想就是将一个大型的任务拆分成很多个小任务,分别执行,最终将小任务的结果进行汇总,生成最终的结果。
本质就是把一个线程的任务拆分成多个小任务,然后由多个线程并发执行,最终将结果进行汇总。
比如 A B 两个线程同时在执行,A 的任务比较多,B 的任务相对较少,B 先执行完毕,这时候 B 去帮助 A 完成任务(将 A 的一部分任务拿过来替 A 执行,执行完毕之后再把结果进行汇总),从而提高效率。
工作窃取
ForkJoin 框架,核心是两个类
- ForkJoinTask (描述任务)
- ForkJoinPool(线程池)提供多线程并发工作窃取
使用 ForkJoinTask 最重要的就是要搞清楚如何拆分任务,这里用的是递归思想。
通过fork方法拆分,通过join方法合并
1、需要创建一个 ForkJoinTask 任务,ForkJoinTask 是一个抽象类,不能直接创建 ForkJoinTask 的实例化对象,开发者需要自定义一个类,继承 ForkJoinTask 的子类 RecursiveTask ,Recursive 就是递归的意思,该类就提供了实现递归的功能。主要业务逻辑放在compute方法中。
package com.southwind;
import java.util.concurrent.RecursiveTask;
/**
* 十亿内的数据求和
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
//起始值
private Long start;
//终止值
private Long end;
//临界值,大于或小于临界值,采用的方法有所差异。小于临界值,我们认为一个线程可以解决;大于一百万就需要工作窃取了
// 临界值这里给一百万,注意下面的下划线,这些是对的,方便我们看位数
private Long temp = 100_0000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
//终止值与起始值的差值小于临界值
if(end - start < temp){
Long sum = 0L;
for(Long i=start; i<=end; i++){
sum += i;
}
return sum;
}else{
//终止值与起始值的差大于临界值,需要找到中间值,然后拆分为2,调用递归运行即可
Long avg = (start + end) / 2;
ForkJoinDemo task1 = new ForkJoinDemo(start, avg);
//让task1、2继续递归
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(avg+1, end);
task2.fork();
//整合结果
return task1.join() + task2.join();
}
}
}
package com.southwind;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
Long startTime = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
//从0加到十亿
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
//让线程池执行任务
forkJoinPool.execute(task);
Long sum = 0L;
try {
//获取执行结果
sum = task.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
Long endTime = System.currentTimeMillis();
System.out.println(sum+",共耗时"+(endTime - startTime));
}
}
运行结果:
500000000500000000,共耗时4549
分析:上面的代码,我们是用forkjoin计算了0到一亿之间数字的和,用时4.5秒
我们如果不使用forkjoin,使用普通方式呢
public class Test2 {
public static void main(String[] args) {
Long startTime = System.currentTimeMillis();
Long sum = 0L;
for(Long i=0L; i<=10_0000_0000L; i++){
sum += i;
}
Long endTime = System.currentTimeMillis();
System.out.println(sum+",共耗时"+(endTime - startTime));
}
}
运行结果:
500000000500000000,共耗时6189
可以看出,我们现在简单的加法运算,forkjoin是有优势的。在实际场景中,业务模型比较复杂,forkjoin的优势会更明显
12、Volatile 关键字
Volatile 是 JVM 提供的轻量级同步机制,可见性,主内存对象线程可见。
不使用volatile
package com.southwind;
import java.util.concurrent.TimeUnit;
public class Test2 {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while(num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
num = 1;
System.out.println(num);
}
}
输出结果:
分析:num初始值为0,我们在主线程里开启一个子线程,子线程在num不等于0的时候退出循环。现在我们有主线程和子线程,主线程休眠1秒,所以子线程获取num值的时候,是去主内存中把num取出到工作内存中,这个时候num是0。然后等主线程休眠结束后,将num改为1,同步回主内存,但是子线程访问的num还是工作内存的num,值还是0。子线程就会死循环下去。
加上volatile
package com.southwind;
import java.util.concurrent.TimeUnit;
public class Test2 {
private static volatile int num = 0;
public static void main(String[] args) {
new Thread(()->{
while(num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
num = 1;
System.out.println(num);
}
}
运行不会出现死循环了。
让我们来看一个反常的例子
package com.southwind;
import java.util.concurrent.TimeUnit;
public class Test2 {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while(num == 0){
System.out.println("---thread---");
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
num = 1;
System.out.println(num);
}
}
上面的代码没加volatile修饰num,然后在子线程的循环当中加了输出语句。看下输出:
---thread---
---thread---
---thread---
---thread---
.........
---thread---
---thread---
1
我们发现最后死循环最后竟然停止了。这是因为一个线程执行完任务之后,会把变量存回到主内存中,并且从主内存中读取当前最新的值,如果是一个空的任务,则不会重新读取主内存中的值。
十三、集合框架
为什么要使用集合框架?
1、数组的长度是固定
2、数组无法同时存储多个不同的数据类型
集合简单理解就是一个长度可以改变,可以保持任意数据类型的动态数组。
集合本身是数据结构的基本概念之一,我们这里说的集合是 Java 语言对这种数据结果的具体实现。
Java 中的集合不是由一个类来完成的,而是由一组接口和类构成了一个框架体系。大致可分为 3 层,最上层是一组接口,继而是接口的实现类。
1、接口
Collection:集合框架最基础的接口,最顶层的接口。
List:Collection 的子接口,存储有序、不唯一(元素可重复)的对象,最常用的接口。
Set:Collection 的子接口,存储无序、唯一(元素不可重复)的对象。
Map:独立于 Collection 的另外一个接口,最顶层的接口,存储一组键值对象,提供键到值的映射。
Iterator:输出集合元素的接口,一般适用于无序集合,从前往后输出。
ListIterator:Iterator 子接口,可以双向输出集合中的元素。
Enumeration:传统的输出接口,已经被 Iterator 取代。
SortedSet:Set 的子接口,可以对集合中的元素进行排序。
SortedMap:Map 的子接口,可以对集合中的元素进行排序。
Queue:队列接口。
Map.Entry:Map 的内部接口,描述 Map 中存储的一组键值对元素。
2、Collection 接口
Collection 是集合框架中最基础的父接口,可以存储一组无序,不唯一的对象。
Collection 接口可以存储一组无序,不唯一(可重复)的对象,一般不直接使用该接口,也不能被实例化,只是用来提供规范。
Collection 是 Iterable 接口的子接口。
int size() | 获取集合长度 |
---|---|
boolean isEmpty() | 判断集合是否为空 |
boolean contains(Object o) | 判断集合中是否存在某个对象 |
Iterator iterator() | 实例化 Iterator 接口,遍历集合 |
Object[] toArray() | 将集合转换为一个 Object 数组 |
T[] toArray(T[] a) | 将集合转换为一个指定数据类型的数组 |
boolean add(E e) | 向集合中添加元素 |
boolean remove(Object o) | 从集合中删除元素 |
boolean containsAll(Collection c) | 判断集合中是否存在另一个集合的所有元素 |
boolean addAll(Collection c) | 向集合中添加某个集合的所有元素 |
boolean removeAll(Collection c) | 从集合中删除某个集合的所有元素 |
void clear() | 清除集合中的所有元素 |
boolean equals(Collection c) | 判断两个集合是否相等 |
int hashCode() | 返回集合的哈希值 |
3、Collection 子接口
List:存放有序、不唯一的元素
Set:存放无序、唯一的元素
Queue:队列接口
4、List 接口
List 常用的扩展方法:
方法 | 含义 |
---|---|
T get(int index) | 通过下标返回集合中对应位置的元素 |
T set(int index,T element) | 在集合中的指定位置存入对象,新值替换旧值 |
int indexOf(Object o) | 从前向后查找某个对象在集合中的位置 |
int lastIndexOf(Object o) | 从后向前查找某个对象在集合中的位置 |
ListIterator listIterator() | 实例化 ListIterator 接口,用来遍历 List 集合 |
List subList(int fromIndex,int toIndex) | 通过下标截取 List 集合 |
T add(int index,T element) | 在集合中的指定位置存入对象,该位置后面的元素往后移,index位置存放当前元素 |
5、List 接口的实现类
1、ArrayList
ArrayList 是开发中使用频率最高的 List 实现类(因为在实际开发中,我们都是通过arrayList进行读取的,arrayList读取快,而对数据库进行增删改是通过数据库操作的,不操作arrayList),实现了长度可变的数组,在内存中分配连续空间,所以读取快,增删慢。
ArrayList:基于数组的实现,非线程安全,效率高,所有的方法都没有 synchronized 修饰。
2、Vector
线程安全,效率低,实现线程安全直接通过 synchronized 修饰方法来完成。底层数据结构为数组。
3、Stack
Vector 的子类,实现了栈的数据结构(后进先出)
push:入栈方法
peek:取出栈顶元素,将栈顶复制一份取出,取完之后栈内的数据不变。
pop:取出栈顶元素,直接取出栈顶元素,取完之后栈内的数据减一。
4、LikedList
实现了先进先出的队列,采用链表的形式存储。非线程安全。
ArrayList 和 LikedList 的区别:内存中存储的形式不同,ArrayList 采用的数组的方式,LinkedList 采用的是链表的形式。
数组在内存中存储空间是连续的,读取快,增删慢。
因为数组在内存中是连续的,所以取数据可以通过寻址公式很快求出目标元素的内存地址,因为内存是连续的,所以新增或者删除元素,必然需要移动数据,而且数组长度越长,需要移动的元素越多,操作就越慢。
LinkedList 和 Stack 都有 pop 方法,有什么区别和相同点?
pop 方法都是取出集合中的第一个元素,但是两者的顺序是相反的,Stack 是“后进先出”,所以 pop 取出的是最后一个元素,LinkedList 是“先进先出”,所以 pop 取出的是第一个元素。
LinkedList 实现了 Deque 接口,而 Deque 接口是 Queue 的子接口,Queue 就是队列,底层实现了队列的数据结构。
5、Queue
实际开发中,不能直接实例化 Queue 对象。
Queue 的实现类是 AbstractQueue,它是一个抽象类,不能直接实例化,开发中需要实现它的子类 PriorityQueue。
Queue 中添加的数据必须是有顺序的(默认会给元素进行升序排序)。例如加数字的话,是可以知道大小的,即有顺序的。
添加数字,有顺序
package com.southwind;
import java.util.*;
public class Test2 {
private static int num = 0;
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue();
queue.add(1);
queue.add(2);
System.out.println(queue);
}
}
添加对象,没有顺序,抛异常
package com.southwind;
import java.util.*;
public class Test2 {
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue();
queue.add(new A(1));
queue.add(new A(2));
System.out.println(queue);
}
}
class A{
private int num;
public A(int num) {
this.num = num;
}
}
上面的代码,我们如果想用,就必须让类A实现comparable接口,重写compareTo方法,自定义比较规则。
package com.southwind;
import java.util.*;
public class Test2 {
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue();
queue.add(new A(1));
queue.add(new A(2));
System.out.println(queue);
}
}
class A implements Comparable{
private int num;
public A(int num) {
this.num = num;
}
@Override
public int compareTo(Object o) {
A a = (A) o;
if(this.num > a.num){
return 1;
}
return 0;
}
}
6、Set
跟 List 一样,Set 是 Collection 的子接口,Set 集合是以散列的形式存储数据,所以元素是没有顺序的,可以存储一组无序且唯一的数据。
Set 常用实现类:
- HashSet
- LinkedHashSet
- TreeSet
1、HashSet
HashSet 是开发中经常使用的一个实现类,存储一组无序且唯一的对象。
无序:元素的存储顺序和遍历顺序不一致。
2、LinkedHashSet
LinkedHasSet 是 Set 的另外一个实现类,可以存储一组有序且唯一的元素.
有序:元素的存储顺序和遍历顺序一致。
equals 和 == 的区别
所有类中的 equals 都是继承自 Object 类,Object 类中原生的 eqauls 方法就是在通过 == 进行判断
但是每个类都可以对 equals 方法进行重写,覆盖掉之前使用 == 进行判断的逻辑,改用新的逻辑进行判断是否相等。
什么是 hashCode?
将对象的内部信息(内存地址、属性值等),通过某种特定规则转换成一个散列值,就是该对象的 hashCode。
两个不同对象的 hashCode 值可能相等。
hashCode 不相等的两个对象一定不是同一个对象。
LinkedHashSet 如何判断两个对象是否相等?
1、会先比较他们的 hashCode,如果 hashCode 不相等,则认为不是同一个对象,可以添加。
2、如果 hashCode 值相等,还不能认为两个对象是相等的,需要通过 equals 进行进一步的判断,equals 相等,则两个对象相等,不可以添加;否则两个对象不相等,可以添加。
3、TreeSet
LinkedHashSet 和 TreeSet 都是存储一组有序且唯一的数据,但是这里的两个有序是有区别的。
LinkedHashSet 的有序是指元素的存储顺序和遍历顺序是一致的。
如:6,3,4,5,1,2–>6,3,4,5,1,2
TreeSet 的有序是指集合内部会自动对所有的元素按照升序进行排列,无论存入的顺序是什么,遍历的时候一定按照升序输出。
list的有序是指内存地址的连续,下标都是连续的;set的有序,像linkedHashSet是存储顺序和遍历顺序一致;treeSet的有序是对元素进行升序排序了。
TreeSet报错例子
package com.southwind;
import java.util.*;
public class Test {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(new A(1));
treeSet.add(new A(3));
treeSet.add(new A(2));
treeSet.add(new A(4));
System.out.println(treeSet.size());
}
}
class A {
private int num;
public A(int num) {
this.num = num;
}
}
这是因为TreeSet会对存入的元素进行升序排序,我们存入的是类A的对象,这些对象没法比较,就报错了。解决方法就是类A实现Comparable接口,重新CompareTo方法。
package com.southwind;
import java.util.*;
public class Test {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(new A(1));
treeSet.add(new A(3));
treeSet.add(new A(2));
treeSet.add(new A(4));
Iterator iterator = treeSet.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class A implements Comparable{
private int num;
public A(int num) {
this.num = num;
}
@Override
public int compareTo(Object o) {
A a = (A) o;
if(this.num > a.num) {
return 1;
} else if (this.num == a.num) {
return 0;
}else {
return -1;
}
}
@Override
public String toString() {
return "A{" +
"num=" + num +
'}';
}
}
输出结果:
A{num=1}
A{num=2}
A{num=3}
A{num=4}
7、Map
key-value,数据字典
List、Set 接口都是 Collection 的子接口,Map 接口是与 Collection 完全独立的另外一个体系。
List & Set VS Map
List & Set & Collection 只能操作单个元素,Map 可以操作一对元素,因为 Map 存储结构是 key - value 映射。
Map 接口定义时使用了泛型,并且定义两个泛型 K 和 V,K 表示 key,规定键元素的数据类型,V 表示 value,规定值元素的数据类型。
方法 | 描述 |
---|---|
int size() | 获取集合长度 |
boolean isEmpty() | 判断集合是否为空 |
boolean containsKey(Object key) | 判断集合中是否存在某个 key |
boolean containsValue(Object value) | 判断集合中是否存在某个 value |
V get(Object key) | 取出集合中 key 对应的 value |
V put(K key,V value) | 向集合中存入一组 key-value 的元素 |
V remove(Object key) | 删除集合中 key 对应的 value |
void putAll(Map map) | 向集合中添加另外一个 Map |
void clear() | 清除集合中所有的元素 |
Set keySet() | 取出集合中所有的 key,返回一个 Set |
Collection values() | 取出集合中所有的 value,返回一个 Collection |
Set<Map.Entry<K,V>> entrySet() | 将 Map 以 Set 的形式输出 |
int hashCode() | 获取集合的散列值 |
boolean equals(Object o) | 比较两个集合是否相等 |
1、Map 接口的实现类
HashMap:存储一组无序,key 不可以重复,value 可以重复的元素。
Hashtable:存储一组无序,key 不可以重复,value 可以重复的元素。
TreeMap:存储一组有序,key 不可以重复,value 可以重复的元素,可以按照 key 进行排序。
Hashtable 用法与 HashMap基本一样,它们的区别是,Hashtable是线程安全的,但是性能较低。HashMap 是非线程安全的,但是性能较高。
HashMap,方法没有用 synchronized 修饰,所以是非线程安全的。
Hashtable,方法用 synchronized 修饰,所以是线程安全的。
TreeMap 主要功能是按照 key 对集合中的元素进行排序。
2、HashMap存储自定义对象
如果HashMap的键存储的是自定义对象,就需要重写hashcode和equals方法;如果值存储的是自定义对象,则不需要重写
因为HashMap的底层和HashSet类似,都是哈希表。存入对象的时候,会先创建一个entry对象。然后使用计算key的哈希值hashcode,如果对应哈希值位置没有元素则添加该entry对象;如果对应哈希值位置有元素,则会调用equals方法来比较key是否相等,相等则将新元素覆盖老元素;不相等则添加该元素。在整个判断过程中,使用到了hashcode和equals方法,所以要进行重写,以满足业务要求。
public class Student {
private String name;
private int age;
public Student() {
}
public Student(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 "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//注意,下面的hashCode和equals重写,是由IDEA自动给我们生成的。点击generate即可生成
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Student student = (Student) object;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public static void main(String[] args) {
HashMap<Student, String> hashMap = new HashMap<>();
Student s1 = new Student("zhangsan", 23);
Student s2 = new Student("lisi", 24);
Student s3 = new Student("wangwu", 25);
Student s4 = new Student("wangwu", 25);
hashMap.put(s1, "江苏");
hashMap.put(s2, "上海");
hashMap.put(s3, "福建");
hashMap.put(s4, "山东");
for(Student s : hashMap.keySet()){
System.out.println(s + "=" + hashMap.get(s));
}
}
//输出结果:
Student{name='wangwu', age=25}=山东
Student{name='lisi', age=24}=上海
Student{name='zhangsan', age=23}=江苏
8、Collections 工具类
Collection 接口,List 和 Set的父接口。
Collections不是接口,它是一个工具类,专门提供了一些对集合的操作,方便开发者去使用,完成相应的业务功能。
Colletions 针对集合的工具类,Collection
Arrays 针对数组的工具类,Array
name | 描述 |
---|---|
public static sort() | 对集合进行排序 |
public static int binarySearch(List list,Object v) | 查找 v 在 list 中的位置,集合必须是升序排列 |
public static get(List list,int index) | 返回 list 中 index 位置的值 |
public static void reverse(List list) | 对 list 进行反序输出 |
public static void swap(List list,int i,int j) | 交换集合中指定位置的两个元素 |
public static void fill(List list,Object obj) | 将集合中所有元素替换成 obj |
public static Object min(List list) | 返回集合中的最小值 |
public static Object max(List list) | 返回集合中的最大值 |
public static boolean replaceAll(List list,Object old,Object new) | 在 list 集合中用 new 替换 old |
public static boolean addAll(List list,Object… obj) | 向集合中添加元素 |
可变参数,在调用方法的时候,参数可以是任意个数,但是类型必须匹配。
public static void test(int... arg){
}
但是下面这种写法,可以传任意类型,任意数量的参数,多态的一种具体表示形式。
public static void test(Object... arg){
}
如何取出可变参数?将可变参数看成数组就行,对数组进行操作。
JavaScript js 脚本语言
Java 是必须全部编译之后,统一执行,假如有 10 行 Java 代码,必须先对这 10 行代码进行编译,通过之后,再交给 JVM 执行。(必须编译通过才能逐行执行)
JS 逐行执行,执行一行算一行,假如有 10 行 JS 代码,一行一行开始执行,执行到第 5 行报错,那么后续 6-10 就不再执行,但是已经执行的前 5 行结果不变。
Java 更加严谨,JS 更加随意
Java 是强语言类型的,JS 是弱语言类型
十四、泛型
泛型(Generics),是指在类定义时不指定类中信息的具体数据类型,而是暂时用一个标识符来替代,当外部实例化对象的时候再来指定具体的数据类型。
不使用泛型
//定义 A 类的时候就指定了属性是 B 类型
public class A{
private B b;
public C test(D d){
return new C();
}
}
使用泛型
//定义 A 类的时候不指定属性的类型
public class A<T,E,M>{
private T b;
public E test(M m){
return E;
}
}
A<B,C,D> a = new A();
优点:这样做极大地提升程序的灵活性,提升类的扩展性,泛型可以指代类中成员变量的数据类型,方法的返回值类型以及方法的参数类型。
1、泛型的应用
自定义类中添加泛型
public class 类名<泛型1,泛型2,泛型3...>{
private 泛型1 属性名;
public 泛型2 方法名(泛型3){
方法体
}
}
2、泛型通配符
有一个参数为 ArrayList 的方法,希望这个方法即可接收泛型是 String 的集合,又可以接收泛型是 Integer 的集合,怎么实现?
多态在泛型中不适用
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
test(list1);
test(list2);
}
public static void test(ArrayList<?> arrayList){
}
}
ArrayList<?> 表示可以使用任意的泛型类型对象,这样 test 方法具备通用性了。
3、泛型上限和下限
上限:表示实例化时具体的数据类型,可以是上限类型的子类或者是上限类型本身,用 extends 表示。
下限:表示实例化时具体的数据类型,可以是下限类型的父类或者是下限类型本身,用 super 表示。
上限:类名<泛型标识 extends 上限类名>
下限:类名<泛型标识 super 下限类名>
package com.southwind;
public class Test<T> {
public static void main(String[] args) {
//泛型上限,传入Number本身
test(new Test<Number>());
//泛型上限,传入Number子类
test(new Test<Integer>());
//泛型下限,传入String本身
test2(new Test<String>());
//泛型下限,传入String父类Object
test2(new Test<Object>());
}
//上限
public static void test(Test<? extends Number> test){
}
//下限
public static void test2(Test<? super String> test){
}
}
4、泛型接口
接口
public interface MyInterface<T> {
public T getValue();
}
实现泛型接口有两种方式:
- 实现类在定义时继续使用泛型标识
public class MyInterfaceImpl<T> implements MyInterface<T>{
@Override
public T getValue() {
return null;
}
}
- 实现类在定义时直接给出具体的数据类型
public class MyInterfaceImpl implements MyInterface<String>{
@Override
public String getValue() {
return null;
}
}
十五、Java 实用类
1、枚举
枚举 Enum,是一种有确定值区间的数据类型,本质上就是一个类,具有简洁、安全、方便等特点。
枚举的值被约束到了一个特定的范围内,只能从这个范围以内取值。
为什么要有枚举?
因为在描述某些对象的属性时,该属性的值不能随便定义,必须在某个特定的区间内取值。
出于对数据的安全性考虑,类似这种有特定取值范围的数据我们就可以使用枚举来描述。
枚举指由一组常量组成的类型,指定一个取值区间,我们只能从该区间中取值。
public enum Week {
MONDAY,
TUESDAY,
WEDNSDAY;
}
public class Test {
public static void main(String[] args) {
System.out.println(Week.MONDAY);
}
}
输出结果:MONDAY
2、String
Java 通过 String 类来创建和操作字符串数据。
String 实例化
- 直接赋值
String str = "Hello World";
- 通过构造函数创建对象
String str = new String("Hello World");
例子:
public class Test {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
String str3 = new String("world");
String str4 = new String("world");
System.out.println(str3 == str4);
}
}
结果:
true
false
分析:
java针对字符串,有一个字符串常量池,它位于堆内存中。执行String str1 = “hello”;这一行代码,先会在字符串常量池中找是否有hello,没有则创建并将hello的地址返回。执行String str2 = “hello”;发现字符串常量池中有hello直接返回地址,所以str1 == str2
通过new的方式是在堆内存中创建了2个world,地址不一样。
String 常用方法
方法 | 描述 |
---|---|
public String() | 创建一个空的字符串对象 |
public String(String value) | 创建一个值为 value 的字符串对象 |
public String(char value[]) | 将一个char数组转换为字符串对象 |
public String(char value[],int offset, int count) | 将一个指定范围的char数组转为字符串对象 |
public String(byte value[]) | 将一个byte数组转换为字符串对象 |
public String(byte value[],int offset, int count) | 将一个指定范围的byte数组转为字符串对象 |
public int length() | 获取字符串的长度 |
public boolean isEmpty() | 判断字符串是否为空 |
public char charAt(int index) | 返回指定下标的字符 |
public byte[] getBytes() | 返回字符串对应的byte数组 |
public boolean equals(Object anObject) | 判断两个字符串值是否相等 |
public boolean equalsIgnoreCase(Object anObject) | 判断两个字符串值是否相等(忽略大小写) |
public int compareTo(String value) | 对字符串进行排序 |
public int compareToIgnoreCase(String value) | 忽略大小写进行排序 |
public boolean startsWith(String value) | 判断字符串是否以 value 开头 |
public boolean endsWith(String value) | 判断字符串是否以 value 结尾 |
public int hashCode() | 返回字符串的 hash 值 |
public int indexOf(String str) | 返回 str 在字符串中的下标 |
public int indexOf(String str,int formIndex) | 从指定位置查找字符串的下标 |
public String subString(int beginIndex) | 从指定位置开始截取字符串 |
public String subString(int beginIndex,int endIndex) | 截取指定区间的字符串 |
public String concat(String str) | 追加字符串 |
public String replaceAll(String o,String n) | 将字符串中所有的 o 替换成 n |
public String[] split(String regex) | 用指定的字符串对目标进行分割,返回数组 |
public String toLowerCase() | 转小写 |
public String toUpperCase() | 转大写 |
public char[] toCharArray() | 将字符串转为字符数组 |
null 和空是两个概念。
null 是指对象不存在,引用地址为空。
空是指对象存在,没有内容,长度为零。
字符串底层
例子1:
String s = "a" + "b" + "c";
System.out.println(s);
拼接的时候没有变量,都是字符串,会触发字符串的优化机制:在编译的时候就已经确定了最终结果
例子2:
String s1 = "a";
String s2 = s1 + "b";
String s3 = s2 + "c";
System.out.println(s3);
执行String s1 = “a”;会在栈内存中创建变量s1,在字符串常量池中创建字符串a。
JDK8以前,执行String s2 = s1 + “b”;会在栈内存中创建变量s2,在字符串常量池中创建字符串b,然后进行s1和b的拼接。拼接过程如下:首先创建一个空的StringBuilder对象,然后调用StringBuilder的append方法将字符串s1和b加入,这个时候还是StringBuilder对象,然后调用toString方法转换回String。在这一个加号拼接过程中,新创建了2个对象,一个是StringBuilder对象,另一个是最后的toString底层是new String,又会创建一个对象。
执行String s3 = s2 + “c”;原理和上述类似。
String s1 = "a";
String s2 = s1 + "b";
String s3 = s2 + "c";
System.out.println(s3);
JDK8会进行优化,像上面的字符串拼接,会在拼接之前先对结果长度进行预估,预估s2的长度是2,于是创建一个长度为2的数组,最后将数组中的值转换回字符串。但是这种优化,底层还是要创建多个对象,浪费空间,时间也很慢
例子3:
StringBuilder sb = new StringBuilder();
sb.append("a");
sb.append("b");
sb.append("c");
System.out.println(sb);
通过StringBuilder方式,首先创建一个空的StringBuilder,然后将a、b、c添加到StringBuilder中
例子4:
String s1 = "abc";
String s2 = "ab";
String s3 = s2 + "c";
System.out.println(s1 == s3); //false
s1是在字符串常量池中,s3是由s2和c进行拼接,无论是JDK8之前通过StringBuilder,还是JDK8通过预估、数组进行优化,最终都会调用new String重新生产一个字符串,新生成的字符串在堆中,所以为false
3、StringBuilder
new StringBuilder()会创建一个容量为16的数组。此时往StringBuilder中添加abc,StringBuilder的容量为16(容量表示最多能存多少),而长度为3(长度表示实际存了多了)
new StringBuilder()会创建一个容量为16的数组。此时往StringBuilder中添加a-z一共26个字母,已经超过StringBuilder的容量了,此时StringBuilder会自动进行扩容,扩容后的容量=扩容前容量 x 2 + 2=34
new StringBuilder()会创建一个容量为16的数组。此时往StringBuilder中添加a-z和0-9一共36个字母,已经超过StringBuilder的容量了,并且要求的容量已经大于扩容前容量 x 2 + 2,则扩容后的容量=要求的容量36
总结:
- StringBuilder默认创建一个长度为16的数组
- 添加的内容长度小于16,直接存
- 添加的内容长度大于16,会扩容(原来容量x2 + 2)
- 如果扩容之后还不够,则以实际需要的长度为准
4、StringBuffer
String 对象一旦创建,值不能修改(原来的值不能修改,一旦修改就是一个新的对象,只要一改动,就会创建一个新的对象)
修改之后会重新开辟内存空间来存储新的对象,会修改 String 的引用。
String 的值为什么不能修改?修改之后会创建一个新的对象?而不是在原有对象的基础上进行修改?
因为 String 底层是用数组来存值的,数组长度一旦创建就不可修改,所以导致上述问题。
StringBuffer 可以解决 String 频繁修改造成的空间资源浪费的问题。
StringBuffer 底层也是使用数组来存值。
- StringBuffer 数组的默认长度为 16,使用无参构造函数来创建对象。
- 使用有参构造创建对象,数组长度=值的长度+16。
package com.southwind.demo5;
public class Test {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
StringBuffer stringBuffer1 = new StringBuffer("hello");
stringBuffer.append("hello");
System.out.println(stringBuffer.length());
System.out.println(stringBuffer1.length());
}
}
输出结果:
5
5
上述代码,stringBuffer的底层数组长度是16,stringBuffer1的底层数组长度是21。length 方法返回的并不是底层数组的长度,而是它的有效长度(值的长度)。
StringBuffer 一旦创建,默认会有 16 个字节的空间去修改,但是一旦追加的字符串长度超过 16,如何处理?
StringBuffer 不会重新开辟一块新的内存区域,而是在原有的基础上进行扩容,通过调用父类 ensureCapacityInternal() 方法对底层数组进行扩容,保持引用不变。
StringBuffer 的常用方法,StringBuffer 是线程安全的,但是效率较低,StringBuilder 是线程不安全的,但是效率较高。
HashMap:线程不安全,效率高
Hashtable:线程安全,效率低
方法 | 描述 |
---|---|
public StringBuffer() | 创建一个空的 StringBuffer对象 |
public StringBuffer(String str) | 创建一个值为 str 的 StringBuffer 对象 |
public synchronized int length() | 返回 StringBuffer 的长度 |
public synchronized char charAt(int index) | 返回指定位置的字符 |
public synchronized StringBuffer append(String str) | 追加内容 |
public synchronized StringBuffer delete(int start,int end) | 删除指定区间的值 |
public synchronized StringBuffer deleteCharAt(int index) | 删除指定位置的字符 |
public synchronized StringBuffer replace(int start,int end,String str) | 将指定区间的值替换成 str |
public synchronized String substring(int start) | 截取字符串从指定位置到结尾 |
public synchronized String substring(int start,int end) | 截取字符串从start开始,到end结束 |
public synchronized StringBuffer insert(int offset,String str) | 在指定位置插入 str |
public int indexOf(String str) | 从头开始查找指定字符的位置 |
public int indexOf(String str,int fromIndex) | 从fromIndex开始查找指定字符的位置 |
public synchronized StringBuffer reverse() | 进行反转 |
public synchronized String toString() | 转为 String |
读取数据不需要考虑线程安全问题,因为这种操作不存在安全隐患。
5、日期类
- java.util.Date
Date 对象表示当前的系统时间
package com.southwind.demo5;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date();
//h是12小时值,H是24小时制
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String myDate = simpleDateFormat.format(date);
System.out.println(myDate);
}
}
输出结果:
2024-01-05 22:08:34
- java.util.Calendar
Calendar 用来完成日期数据的逻辑运算(传入日期,传出转换后的日期)
运算思路:
1、将日期数据传给 Calendar(Calendar 提供了很多静态常量,专门用来记录日期数据)
常量 | 描述 |
---|---|
public static final int YEAR | 年 |
public static final int MONTH | 月 |
public static final int DAY_OF_MONTH | 天,以月为单位(今天在这个月是第几天) |
public static final int DAY_OF_YEAR | 天,以年为单位(今天在这一年是第几天) |
public static final int HOUR_OF_DAY | 小时(24小时制) |
public static final int MINUTE | 分钟 |
public static final int SECOND | 秒 |
public static final int MILLSECOND | 毫秒 |
2、调用相关方法进行运算
方法 | 描述 |
---|---|
public static Calendar getInstance() | 获取Calendar实例化对象 |
public void set(int field,int value) | 给静态常量赋值 |
public int get(int field) | 获取静态常量的值 |
public final Date getTime() | 将Calendar转为Date对象 |
package com.southwind.demo5;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class Test {
public static void main(String[] args) {
//计算今天所在的周是2024年的第几周
Calendar calendar = Calendar.getInstance();
//将2024赋值给年
calendar.set(Calendar.YEAR, 2024);
//将月传入,1月为0,2月为1,以此类推
calendar.set(Calendar.MONTH, 0);
//将日传入
calendar.set(Calendar.DAY_OF_MONTH, 5);
//获取结果
int week = calendar.get(Calendar.WEEK_OF_YEAR);
System.out.println(week);
//今天的63天后是几月几日
//获取今天是一年的第几天
int days = calendar.get(Calendar.DAY_OF_YEAR);
days += 63;
//将63天后的日期存入calendar
calendar.set(Calendar.DAY_OF_YEAR, days);
Date date = calendar.getTime();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(simpleDateFormat.format(date));
}
}
输出结果:
1
2024-03-08
十六、文件
1、文件
File 类
java.io.File,使用该类的构造函数就可以创建文件对象,将硬盘
中的一个具体的文件
以 Java 对象的形式来表示。
方法 | 描述 |
---|---|
public File(String pathname) | 根据路径创建对象 |
public String getName() | 获取文件名 |
public String getParent() | 获取文件所在的目录 |
public File getParentFile() | 获取文件所在目录对应的File对象 |
public String getPath() | 获取文件路径 |
public boolean exists() | 判断文件是否存在 |
public boolean isDirectory() | 判断对象是否为目录 |
public boolean isFile() | 判断对象是否为文件 |
public long length() | 获取文件的大小 |
public boolean createNewFile() | 根据当前对象创建新文件 |
public boolean delete() | 删除对象 |
public boolean mkdir() | 根据当前对象创建目录 |
public boolean renameTo(File file) | 为已存在的对象重命名 |
2、IO
Input 输入流(将外部文件读入到 Java 程序中)
Output 输出流(将 Java 程序中的数据输出到外部)
Java 中的流有很多种不同的分类。
- 按照方向分,
输入流
和输出流
- 按照单位分,可以分为
字节流
和字符流
(字节流是指每次处理数据以字节为单位,字符流是指每次处理数据以字符为单位) - 按照功能分,可以分为
节点流
和处理流
。
3、字节流
按照方向可以分为输入字节流InputStream和输出字节流OutputStream。
1 byte = 8 位二进制数 01010101
InputStream常用方法
方法 | 描述 |
---|---|
int read() | 以字节为单位读取数据 |
int read(byte b[]) | 读取数据,并将数据存入 byte 类型的数组中,返回数组中有效数据的长度 |
int read(byte b[],int off,int len) | 将数据存入 byte 数组的指定区间内,返回数组长度 |
byte[] readAllBytes() | 将所有数据存入 byte 数组并返回 |
int available() | 返回当前数据流未读取的数据个数 |
void close() | 关闭数据流 |
package com.southwind.demo6;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception {
File file = new File("C:\\Users\\gaoyao\\Desktop\\test.txt");
if(file.exists()){
InputStream inputStream = new FileInputStream(file);
//不推荐for循环
// long len = file.length();
// for(int i=0; i<len; i++){
// System.out.println(inputStream.read());
// }
int temp = 0;
while ((temp = inputStream.read()) != -1){
System.out.println(temp);
}
//关闭流
inputStream.close();
}
}
}
OutputStream
方法 | 描述 |
---|---|
void write(int b) | 以字节为单位输出数据 |
void write(byte b[]) | 将byte数组中的数据输出 |
void write(byte b[],int off,int len) | 将byte数组中指定区间的数据输出 |
void close() | 关闭数据流 |
void flush() | 将缓冲流中的数据同步到输出流中 |
package com.southwind.demo6;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class Test2 {
public static void main(String[] args) throws Exception{
OutputStream outputStream = new FileOutputStream("C:\\Users\\gaoyao\\Desktop\\target.txt");
outputStream.write(96);
outputStream.close();
}
}
该方法会创建target.txt文件,并将96以ASCII码形式存入到文本中。
4、字符流
字节流是单位时间内处理一个字节的数据(输入+输出)
字符流是单位时间内处理一个字符的数据(输入+输出)
字符流:
- 输入字符流 Reader
- 输出字符流 Writer
5、Reader
是一个抽象类。Readable 接口的作用?
可以将数据以字符的形式读入到缓冲区
package com.southwind.demo7;
import java.io.FileReader;
import java.io.Reader;
public class Test {
public static void main(String[] args) throws Exception{
Reader reader = new FileReader("C:\\Users\\gaoyao\\Desktop\\test.txt");
int temp = 0;
while((temp = reader.read()) != -1){
System.out.println(temp);
}
reader.close();
}
}
6、Writer
Appendable 接口可以将 char 类型的数据读入到数据缓冲区
package com.southwind.demo7;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.Reader;
import java.io.Writer;
public class Test {
public static void main(String[] args) throws Exception{
Writer writer = new FileWriter("C:\\Users\\gaoyao\\Desktop\\copy.txt");
writer.write("hello world");
writer.close();
}
}
7、处理流
读文件**
package com.southwind.demo7;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception{
//基础管道
InputStream inputStream = new FileInputStream("C:\\Users\\gaoyao\\Desktop\\copy.txt");
//处理流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
char[] arr = new char[15];
inputStreamReader.read(arr);
inputStreamReader.close();
inputStream.close();
System.out.println(arr);
}
}
写文件
package com.southwind.demo7;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception{
String str = "你好";
OutputStream outputStream = new FileOutputStream("C:\\Users\\gaoyao\\Desktop\\copy.txt");
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
writer.write(str);
writer.flush();
writer.close();
outputStream.close();
}
}
8、缓冲流
无论是字节流
还是字符流
,使用的时候都会频繁访问硬盘,对硬盘是一种损伤,同时效率不高,如何解决?
可以使用缓冲流,缓冲流自带缓冲区,可以一次性从硬盘中读取部分数据存入缓冲区,再写入内存,这样就可以有效减少对硬盘的直接访问。
缓冲流属于处理流
,如何来区分节点流和处理流?
1、节点流使用的时候可以直接对接到文件对象File
2、处理流使用的时候不可以直接对接到文件对象 File,必须要建立在字节流
的基础上才能创建。
缓冲流又可以分为字节缓冲流和字符缓冲流,按照方向再细分,又可以分为字节输入缓冲流和字节输出缓冲流,以及字符输入缓冲流和字符输出缓冲流。
9、序列化和反序列化
序列化就是将内存中的对象输出到硬盘文件中保存。
反序列化就是相反的操作,从文件中读取数据并还原成内存中的对象。
序列化
1、实体类需要实现序列化接口,Serializable
package com.southwind.demo8.entity;
import java.io.Serializable;
public class User implements Serializable {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
2、实体类对象进行序列化处理,通过数据流写入到文件中,ObjectOutputStream。
package com.southwind.demo8.entity;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
public class Test {
public static void main(String[] args) throws Exception{
User user = new User(1, "张三", 22);
OutputStream outputStream = new FileOutputStream("C:\\Users\\gaoyao\\Desktop\\obj.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(user);
objectOutputStream.flush();
objectOutputStream.close();
outputStream.close();
}
}
反序列化
package com.southwind.demo8.entity;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
public class Test2 {
public static void main(String[] args) throws Exception{
InputStream inputStream = new FileInputStream("C:\\Users\\gaoyao\\Desktop\\obj.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
User user = (User)objectInputStream.readObject();
System.out.println(user);
objectInputStream.close();
inputStream.close();
}
}
10、IO 流的应用
IO 流就是完成文件传输(上传文件:发朋友圈、换头像,文件下载:CSDN 下载源代码、文档)
图片传输
package com.southwind.demo8.entity;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception{
InputStream inputStream = new FileInputStream("C:\\Users\\gaoyao\\Desktop\\1.png");
OutputStream outputStream = new FileOutputStream("C:\\Users\\gaoyao\\Desktop\\copy.png");
int temp = 0;
while ((temp = inputStream.read()) != -1){
outputStream.write(temp);
}
outputStream.flush();
outputStream.close();
inputStream.close();
}
}
十七、反射
地位:Java 中最核心的模块,Java 之所以称为动态语言的关键,大部分的类库、企业级框架底层都是通过反射来实现的,非常重要。
反射顾名思义就反转执行,生活中的反射就是通过虚像映射到具体的实物,可以获取到实物的某些形态特征。
程序中的反射,通过一个实例化对象映射到类。
一句话理解反射:常规情况下是通过类来创建对象的,反射就是将这一过程进行反转,通过对象来获取类的信息(获取目标类的运行时类)。
java程序运行时,首先需要将工程中的所有类加载到JVM内存中,并且只加载一次,加载到内存中的类就叫做运行时类。
通过对象来获取类的信息
类的信息我们也同样使用对象来描述,Class 类专门用来描述其他类的类,每一个 Class 的实例化对象都是对某个类的描述。
Class 是反射的源头
如何来创建 Class 的对象?
1、调用 Class 的静态方法 forName(String name),将目标类的全限定类名(全类名,带着包名的类名)
package com.southwind.demo8.entity;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("com.southwind.demo8.entity.User");
System.out.println(clazz.getName());
System.out.println(clazz.getTypeName());
System.out.println(clazz.getSuperclass().getName());
System.out.println("-----------------");
Class[] array = clazz.getInterfaces();
for(Class t : array){
System.out.println(t);
}
}
}
输出结果:
com.southwind.demo8.entity.User
com.southwind.demo8.entity.User
java.lang.Object
-----------------
interface java.io.Serializable
2、通过目标类的 class 创建,Java 中的每一个类都可以调用类.class,class 不是属性也不是方法,叫做“类字面量”,作用是获取内存中目标类型对象的引用(类的结构)。
public class Test {
public static void main(String[] args) throws Exception{
Class clazz = User.class;
System.out.println(clazz.getName());
}
}
输出结果:
com.southwind.demo8.entity.User
3、通过目标类的实例化对象获取,getClass()
public class Test {
public static void main(String[] args) throws Exception{
User user= new User(1, "张三", 22);
Class clazz = user.getClass();
System.out.println(clazz.getName());
}
}
输出结果:
com.southwind.demo8.entity.User
Class是对类结构的描述
结构包括:构造函数、成员变量、普通方法、继承的父类、实现的接口
1、获取构造函数
Student
package com.southwind.test;
public class Student {
private int id;
private String name;
public Student(){
}
public Student(int id){
this.id = id;
}
public Student(int id,String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
测试类
package com.southwind.test;
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) {
try {
Class clazz = Student.class;
//获取无参构造函数
Constructor constructor = clazz.getConstructor();
//通过构造函数创建对象
Student student1 = (Student) constructor.newInstance();
System.out.println(student1);
//获取有参构造函数并创建对象
Constructor constructor1 = clazz.getConstructor(int.class);
Student student2 = (Student)constructor1.newInstance(1);
System.out.println(student2);
//获取有参构造函数并创建对象
Constructor constructor2 = clazz.getConstructor(int.class, String.class);
Student student3 = (Student)constructor2.newInstance(2, "张三");
System.out.println(student3);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
输出结果:
Student{id=0, name='null'}
Student{id=1, name='null'}
Student{id=2, name='张三'}
clazz.getConstructor()获取的是无参构造函数,所以创建对象的时候constructor.newInstance()括号里面也不能带参数;如果要获取有参构造函数,就需要在getConstructor()中指定参数的类型,例如clazz.getConstructor(int.class),获取入参有int类型的构造函数,通过在创建对象的时候,constructor1.newInstance(1)在括号中指定入参的值。
2、获取成员变量
getField:获取类的公有(public修饰)属性。
getDeclaredField:获取类的任意访问权限修饰符修饰的属性。
Student
package com.southwind.test;
public class Student {
public int id;
private String name;
public Student(){
}
public Student(int id){
this.id = id;
}
public Student(int id,String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
测试类
package com.southwind.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) {
try {
Class clazz = Student.class;
//获取无参构造函数
Field field = clazz.getField("id");
System.out.println(field);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
输出结果:
public int com.southwind.test.Student.id
如果我们将Student类的id改为私有的,测试类不动,然后运行就会报错。
package com.southwind.test;
public class Student {
private int id;
private String name;
public Student(){
}
public Student(int id){
this.id = id;
}
public Student(int id,String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
解决方法就是改用getDeclaredField
package com.southwind.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) {
try {
Class clazz = Student.class;
//获取无参构造函数
Field field = clazz.getDeclaredField("id");
System.out.println(field);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3、给成员变量赋值
Student
package com.southwind.test;
public class Student {
private int id;
private String name;
public Student(){
}
public Student(int id){
this.id = id;
}
public Student(int id,String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
测试类:
package com.southwind.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) {
try {
Class clazz = Student.class;
Constructor constructor = clazz.getConstructor(int.class, String.class);
Student student = (Student) constructor.newInstance(1, "张三");
System.out.println(student);
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(student, "李四");
System.out.println(student);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
输出结果:
Student{id=1, name='张三'}
Student{id=1, name='李四'}
分析:我们通过getDeclaredField获取到私有成员变量name,然后给name赋值。但是name是私有的,是无法直接赋值的,就需要设置field.setAccessible(true),然后才能访问到。这种方法不建议使用。遇到这种情况,建议使用私有属性的set方法进行赋值,见下一点。
4、获取类的普通方法(动态方法调用)
name是私有的,我们通过setName来修改name属性。
Student
package com.southwind.test;
public class Student {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String name;
public Student(){
}
public Student(int id){
this.id = id;
}
public Student(int id,String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
测试类:
package com.southwind.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
try {
Class clazz = Student.class;
Constructor constructor = clazz.getConstructor(int.class, String.class);
Student student = (Student) constructor.newInstance(1, "张三");
System.out.println(student);
Method method = clazz.getMethod("setName", String.class);
method.invoke(student, "李四");
System.out.println(student);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
输出结果:
Student{id=1, name='张三'}
Student{id=1, name='李四'}
十八、String易错题
public class Main {
public static void main(String[] args) {
String a = new String("abc");
test(a);
System.out.println(a);
}
public static void test(String a){
a += "test";
System.out.println(a);
}
}
输出结果:
abctest
abc
分析:注意,String是引用数据类型。我们把字符串a传到test方法中,然后在后面拼了test,输出abctest。此时相当于重新创建了一个字符串abctest,然后a指向abctest,就输出abctest。接着输出a的值是abc,注意这里abc的值没有变化。因为String底层是final byte数组,一旦创建了是不能被修改的,所以输出abc。
String intern();返回一个相同的字符串,该字符串来自字符串常量池。
public static void main(String[] args) {
String s1 = "123";
String s2 = "123";
String s3 = new String("123");
String s4 = new String("123");
String s5 = s1.intern();
String s6 = s3.intern();
System.out.println(s1 == s2);
System.out.println(s3 == s4);
System.out.println(s1 == s5);
System.out.println(s3 == s6);
System.out.println(s1 == s6);
System.out.println(s3 == s5);
}
输出结果:
true
false
true
false
true
false
分析:s1和s2都取自字符串常量池,相等;s3和s4在堆中内存地址不相等;s5是取字符串常量池中的地址,所以s5和s1相等;s6取字符串常量池中的地址,和s3堆内存地址不相等;s6取自字符串常量池,和s1相等;s5取自字符串常量池,s3取自堆内存,地址不相等。
十九、网络编程
使用socket网络编程的具体步骤:
1、建立连接
2、打开socket关联的输入输出流
3、读写数据流的信息
4、关闭数据流和socket
- 基于TCP协议的网络编程
TCPServer
package com.southwind.test;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
//1、创建socket,指定端口
try {
ServerSocket serverSocket = new ServerSocket(6666);
//2、接收客户端请求
Socket socket = serverSocket.accept();
//3、获取客户端消息
InputStream inputStream = socket.getInputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println(message);
dataInputStream.close();
inputStream.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
TCPClient
package com.southwind.test;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) {
//1、创建socket,指定服务器的ip和端口
try {
Socket socket = new Socket("localhost", 6666);
//2、给服务端发消息
String message = "服务端你好,我是客户端";
//3、因为是作为客户端,要给服务端发消息,这里使用输出流
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF(message);
dataOutputStream.close();
outputStream.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
我们先启动服务器server,再启动客户端client,这时服务端就会收到客户端的消息。如果我们不启动服务器,而是直接启动客户端就会报错。因为我们用的是TCP协议,是可靠传输。
- 基于UDP协议的网络编程
Receive
package com.southwind.test;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Receive {
public static void main(String[] args) {
//1、创建数组接收数据
byte[] buff = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buff, buff.length);
//2、创建socket
try {
DatagramSocket datagramSocket = new DatagramSocket(3333);
//3、接收数据
datagramSocket.receive(datagramPacket);
String message = new String(datagramPacket.getData());
System.out.println("我是receive,接收到了"+message);
} catch (SocketException e) {
throw new RuntimeException(e);
}catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Send
package com.southwind.test;
import java.io.IOException;
import java.net.*;
public class Send {
public static void main(String[] args) {
String message = "测试一下";
//指定要发送的主机
InetAddress inetAddress = null;
try {
inetAddress = InetAddress.getByName("localhost");
//数据打包
DatagramPacket datagramPacket = new DatagramPacket(message.getBytes(),
message.getBytes().length, inetAddress, 3333);
//创建socket
DatagramSocket datagramSocket = new DatagramSocket(1111);
datagramSocket.send(datagramPacket);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}catch (SocketException e) {
throw new RuntimeException(e);
}catch (IOException e) {
throw new RuntimeException(e);
}
}
}
这时先启动receive,再启动send,receive就会收到send的消息。如果不启动receive,直接启动send也不会报错,因为用到的是UDP协议,不是可靠传输。
二十、对象的强引用、软引用、弱引用、虚引用
- 强引用StrongReference
这是Java程序中最常见的引用方式,程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被垃圾回收机制回收。
- 软引用SoftReference
当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可以使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
- 弱引用WeakReference
弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,他就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
- 虚引用PhantomReference
虚引用完全类似于没有引用。虚引用对引用对象本身没有太大的影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用只要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,必须和引用队列ReferenceQueue联合使用。程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用的对象是否即将被回收。
弱引用例子:
public static void main(String[] args) {
String str = new String("123");
//创建一个弱引用,引用到字符串123
WeakReference wr = new WeakReference(str); //1
//切断str和字符串123的引用
str = null; //2
//取出弱引用所引用的对象
System.out.println(wr.get()); //3
//强制垃圾回收
System.gc();
System.runFinalization();
//再次取出弱引用所引用的对象
System.out.println(wr.get()); //4
}
输出结果:
123
null
分析:执行到3时,本程序不会导致内存紧张,此时还不会回收wr所引用的对象。所以3处会输出123。在调用垃圾回收之后,就会将弱引用wr所引用的对象回收,输出就是null。
虚引用例子:
public static void main(String[] args) {
String str = new String("123");
//创建引用队列
ReferenceQueue rq = new ReferenceQueue();
//创建一个虚引用,引用到字符串123
PhantomReference pr = new PhantomReference(str, rq);
//切断str和字符串123的引用
str = null;
//取出虚引用所引用的对象,并不能通过虚引用取出被引用的对象
System.out.println(pr.get()); //1
//强制垃圾回收
System.gc();
System.runFinalization();
//垃圾回收之后,虚引用将被放入引用队列
//取出引用队列中最先进入队列的引用与pr进行比较
System.out.println(rq.poll() == pr); //2
}
输出结果:
null
true
二十一、Java基础类库
- Runtime类
Runtime类代表Java程序的运行时环境,每个Java程序都有一个与之对应的Runtime实例。应用程序不能创建自己的Rumtime实例,但可以通过getRuntime()获取与之关联的Runtime对象。
与System类似,Rumtime类提供了gc()和runFinalization()方法通知系统进行垃圾回收,并提供了load(String filename)和loadLibrary(String libname)来加载文件和动态链接库。
此外,Runtime类还有一个功能,可以直接单独启动一个进程来执行操作系统的命令。
public static void main(String[] args) throws IOException {
Runtime runtime = Runtime.getRuntime();
runtime.exec("E:\\Notepad++\\notepad++.exe");
}
二十二、集合操作
- 使用Lambda表达式遍历集合
Java8为Iterable接口新增了一个forEach(Comsumer action)默认方法,该方法所需参数的类型是一个函数式接口。Iterable接口是Collection接口的父接口,所以Collection接口也可以直接调用该方法。
public static void main(String[] args) throws IOException {
List<String> books = new ArrayList<>();
books.add("java");
books.add("python");
books.forEach(obj -> System.out.println(obj));
}
输出结果:
java
python
- 使用Iterator遍历集合
public static void main(String[] args) throws IOException {
List<String> books = new ArrayList<>();
books.add("java");
books.add("python");
Iterator it = books.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
}
输出结果:
java
python
或者
public static void main(String[] args) throws IOException {
List<String> books = new ArrayList<>();
books.add("java");
books.add("python");
Iterator it = books.iterator();
it.forEachRemaining(obj -> System.out.println(obj));
}
输出结果:
java
python
- 使用foreach遍历集合
public static void main(String[] args) throws IOException {
List<String> books = new ArrayList<>();
books.add("java");
books.add("python");
for(String t : books){
System.out.println(t);
}
}
输出结果:
java
python
- 使用Java8新增的Predicate操作集合
Java8为Collection集合新增了一个removeIf(Predicate filter)方法,该方法将会批量删除符合filter条件的所有元素。该方法需要一个Predicate(谓词)对象作为参数,Predicate也是函数式接口,可以使用Lambda表达式作为参数。
public static void main(String[] args) throws IOException {
List<String> books = new ArrayList<>();
books.add("java");
books.add("python");
books.removeIf(ele ->ele.length() > 4);
System.out.println(books);
}
输出结果:
java
- 使用Java8新增的Steam操作集合
Stream中常用的方法:
filter(Predicate predicate):过滤Stream中所有不符合predicate的元素。
anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。
allMatch(Predicate predicate):判断流中是否每一个元素符合Predicate条件。
noneMatch(Predicate predicate):判断流中是否所有元素都不符合Predicate条件。
- 使用Properties读写属性文件
Properties类是Hashtable类的子类,可以把Map对象和属性文件关联起来,从而可以把Map对现在的key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中。
常用方法:
String getProperty(String key):获取Properties中指定属性名的属性值
String getProperty(String key, String defaultValue):获取Properties中指定属性名的属性值,如果找不到指定的key,返回默认值
Object setProperty(String key, String value):设置属性值
void load(InputStream stream):从属性文件(以输入流表示)中加载key-value对,把加载到的key-value对追加到property中
void store(OutputStream stream, String comments):将properties中的key-value对输出到指定的属性文件中(以输出流表示)
public static void main(String[] args) throws IOException {
Properties properties= new Properties();
properties.setProperty("username", "admin");
properties.setProperty("password", "123");
properties.store(new FileOutputStream("C:\\Users\\gaoyao\\Desktop\\code\\学习\\java核心基础\\demo\\a.ini"), "comment line");
Properties properties2 = new Properties();
properties2.setProperty("gender", "male");
properties2.load(new FileInputStream("C:\\Users\\gaoyao\\Desktop\\code\\学习\\java核心基础\\demo\\a.ini"));
System.out.println(properties2);
}
输出结果:
{password=123, gender=male, username=admin}
二十三、try代码块
对于程序中使用到的一些资源(数据库连接、磁盘文件IO等),在使用结束后需要我们手动关闭。
FileInputStream stream = null;
try{
stream = new FileInputStream("1.txt");
}catch(Exception e){
}finally{
//回收资源
if(stream != null){
stream.close();
}
}
但是上述代码过于臃肿,我们可以使用try来优化。
Java7增强了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显示关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。
需要指出的是,为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法。
优化后的代码:
try(FileInputStream stream = new FileInputStream("1.txt")){
}catch(Exception e){
}
二十四、注解
注解(Annotation)是Java代码里的特殊标记。比如@Override、@Test等,作用是:让其他程序根据注解信息来决定怎么执行该程序。
public class Test {
@Test
public void test(){
}
public void test2(){
}
}
例如上面的代码,test()加了注解,表示该方法是要执行的;test2()没加注解,表示该方法不要执行。
注解可以用在类、构造器、方法、成员变量、参数等位置。
自定义注解
就是自己定义注解。
public @interface 注解名称{
public 属性类型 属性名() default 默认值;
}
特殊属性名value:如果注解中只有一个value属性,使用注解时,value名称可以不写。
例如:
public @interface MyTest1 {
String aaa();
boolean bbb() default true;
String[] ccc();
}
注解的使用:
package com.southwind.demo10;
//注解修饰类。因为bbb有默认值,所以可以不赋值。aaa和ccc没有默认值需要赋值
@MyTest1(aaa="hello", ccc = {"java", "python"})
//下面写法等同于@MyTest2(value = "哈哈哈")
@MyTest2("哈哈哈")
public class Test {
//注解修饰方法
@MyTest1(aaa = "你好", bbb = false, ccc = {"111", "222"})
public void run(){
}
}
注解的原理
我们把上面的MyTest1编译生成class字节码,然后进行反编译:
public interface MyTest1 extends Annotaion{
public abstract String aaa();
public abstract boolean bbb();
public abstract String[] ccc();
}
可以看到,注解其实是一个接口,继承于Annotation,里面的属性其实是一些抽象方法。
当我们使用注解的时候,其实是创建了注解的实现类对象,把这些数据封装到了实现类对象中。然后就可以通过调用方法aaa()、bbb()、ccc()获取注解对应属性值。
@MyTest1(aaa="李四", bbb=true, ccc={"1", "2"})
public void test(){
}
元注解
元注解是修改注解的注解。
修饰Test注解的两个注解就是元注解。
常见元注解:
@Target只能用在类上:
@Target(ElementType.TYPE)
public @interface MyTest3 {
}
@MyTest3 //正常
public class Test {
@MyTest3 //报错
private String name;
@MyTest3 //报错
public void run(){
}
}
@Target注解支持用在类上、方法上:就在修改注解的时候,传入一个数组。
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyTest3 {
}
@MyTest3 //正常
public class Test {
@MyTest3 //报错
private String name;
@MyTest3 //正常
public void run(){
}
}
注解的解析
就是判断类、方法、成员变量上是否存在注解,并把注解里的内容给解析出来。
如何解析注解?
要解析谁上面的注解,就也应该先拿到谁。
比如说要解析类上面的注解,就应该先获取该类的Class对象,再通过Class对象解析其上面的注解。
比如说要解析成员方法上面的注解,就应该先获取该成员方法的Method对象,再通过Method对象解析其上面的注解。
Class、Method、Field、Constructor,都实现了AnnotatedElement接口,它们都拥有解析注解的能力。
我们定义了一个注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest4 {
String value();
double aaa() default 100;
String[] bbb();
}
使用注解:
@MyTest4(value = "111", aaa = 99.5, bbb={"c++", "python"})
public class Demo {
@MyTest4(value = "222", aaa = 66.3, bbb={".net", "go"})
public void test1(){
}
}
解析注解:
package com.southwind.demo10;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
public class AnnotationTest {
public static void main(String[] args) throws Exception {
parseClass();
parseMethod();
}
/**
* 解析类上的注解
*/
public static void parseClass(){
//获取Class对象
Class clazz = Demo.class;
//判断类上是否包了某个注解
if(clazz.isAnnotationPresent(MyTest4.class)){
MyTest4 myTest4 = (MyTest4) clazz.getDeclaredAnnotation(MyTest4.class);
//输出注解的值
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(Arrays.toString(myTest4.bbb()));
}
}
/**
* 解析方法上的注解
*/
public static void parseMethod() throws Exception {
//获取Class对象
Class clazz = Demo.class;
Method method = clazz.getDeclaredMethod("test1");
//判断方法上是否包了某个注解
if(method.isAnnotationPresent(MyTest4.class)){
MyTest4 myTest4 = method.getDeclaredAnnotation(MyTest4.class);
//输出注解的值
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(Arrays.toString(myTest4.bbb()));
}
}
}
输出结果:
111
99.5
[c++, python]
222
66.3
[.net, go]
注解的应用场景
例如,我们模拟Junit框架。
定义若干个方法,只有加了MyTest注解,就会触发该方法执行。
定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
}
测试:
package com.southwind.demo10;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class AnnotationTest2 {
@MyTest
public void test1(){
System.out.println("test1");
}
public void test2(){
System.out.println("test2");
}
public static void main(String[] args) throws Exception {
AnnotationTest2 annotationTest2 = new AnnotationTest2();
Class clazz = AnnotationTest2.class;
Method[] methods = clazz.getDeclaredMethods();
//遍历每个方法,判断该方法是否加了@MyTest注解,存在则触发该方法执行
for (Method method : methods) {
if(method.isAnnotationPresent(MyTest.class)){
method.invoke(annotationTest2);
}
}
}
}
输出结果:
test1
test1方法上面加了注解,可以执行;test2方法上面没加注解,不可以执行。
二十五、动态代理
1、程序为什么需要代理?代理长什么样?
比如某个明星,她有唱歌和跳舞两个方法。唱歌和跳舞都要有一些准备场地、收钱等其他杂事,明显不想做这些,只想做唱歌、跳舞。
那么就需要找一个代理。代理负责做这些杂事,明显安心唱歌跳舞。如果外部要调用某明星的唱歌和跳舞方法,需要去调用代理的唱歌方法,然后代理去调用明显的唱歌方法,完成整个过程。
那么代理如果知道要有唱歌、跳舞方法的代理呢?
我们定义一个明显接口,里面定义唱歌和跳舞两个方法。然后明显和代理去实现这个接口就可以了。
接口:
public interface Star {
void sing();
void dance();
}
明星类实现上述接口:
public class BigStar implements Star{
private String name;
public BigStar() {
}
public BigStar(String name) {
this.name = name;
}
public void sing(){
System.out.println(this.name + "正在唱歌");
}
public void dance(){
System.out.println(this.name + "正在跳舞");
}
}
代理工具类:
public class ProxyUtil {
//为明星生成代理
public static Star createProxy(BigStar bigStar){
Star starProxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理要做的事情在这里写
if(method.getName().equals("sing")){
System.out.println("准备话筒,收钱100");
} else if (method.getName().equals("dance")) {
System.out.println("准备场地,收钱200");
}
return method.invoke(bigStar, args);
}
});
return starProxy;
}
}
测试类:
public class Test {
public static void main(String[] args) {
BigStar bigStar = new BigStar("张三");
Star starProxy = ProxyUtil.createProxy(bigStar);
starProxy.sing();
starProxy.dance();
}
}
输出结果:
准备话筒,收钱100
张三正在唱歌
准备场地,收钱200
张三正在跳舞
二十六、java进制
二进制:代码中以0b开头,例如0b11是3,0b123会编译报错
八进制:代码中以0开头,例如017是15
十六进制:代码中以0x开头,例如0x17是23
二十七、隐式类型转换
byte short char三种数据类型在运算时,都会先提升为int再参与运算。例如:
byte a = 10;
byte b = 20;
byte c = (byte)(a + b); //a+b为int类型,需要进行强转为int
二十八、字符串拼接
字符串拼接,是从左到右依次进行。例如:
1+2+“abc” = “3abc”。先计算1+2得到3,然后3与"abc"进行拼接
二十九、运算
+=、-=、*=、/=、%=,这5个计算,底层都隐藏了强制类型转换。例如:
short s = 1;
s += 1; //正确,这行代码等同于s = (short)(s+1);short类型计算之前会转换为int类型,int类型范围大需要进行强制类型转换回short
int a = 300;
String s = "";
for(int i=31; i>=0 ; i--){
String str = ((a & (1 << i)) == 0) ? "0" : "1";
s += str;
}
System.out.println("a的二进制为:" + s);
byte b = (byte)a;
System.out.println(b);
输出结果:
a的二进制为:00000000000000000000000100101100
44
分析:将int类型的a强转为byte类型。int类型占用4个字节即32位,而byte类型占用1个字节即8位。将32位的数字赋值给8位,则只保留最后的8位,即保留00101100,由于计算机中数字都是以补码形式存的,00101100的第1位是符号位,0表示正数,转换为十进制即为44
int a = 200;
String s = "";
for(int i=31; i>=0 ; i--){
String str = ((a & (1 << i)) == 0) ? "0" : "1";
s += str;
}
System.out.println("a的二进制为:" + s);
byte b = (byte)a;
System.out.println(b);
输出结果:
a的二进制为:00000000000000000000000011001000
-56
分析:将int类型的a强转为byte类型。int类型占用4个字节即32位,而byte类型占用1个字节即8位。将32位的数字赋值给8位,则只保留最后的8位,即保留11001000,由于计算机中数字都是以补码形式存的,11001000的第1位是符号位,1表示负数。11001000是补码,对应反码(补码=反码+1)是11000111,对应原码是10111000,转换回十进制为-56
三十、代码块
可以分为:局部代码块、构造代码块、静态代码块
- 局部代码块。可以提前结束变量的声明周期。如下图,在局部代码块在定义了变量a,则出了代码块,变量a就失效了
- 构造代码块(写在一个类成员位置的代码块)
如下图,在无参、有参构造方法中,有公共的逻辑输出语句。我们可以把这段公共逻辑写在构造代码块中。构造代码块会优先构造方法执行。
- 静态代码块
需要通过static关键字修饰,随着类的加载而加载,并且自动触发、只执行一次
使用场景:在类加载时做一些初始化工作
三十一、内部类
内部类表示的事物是外部类的一部分。
分类:成员内部类、静态内部类、局部内部类、匿名内部类。前3个只做了解
public class Outer{
class Inner{
}
}
//我们可以把Inner类看作是Outer的成员,要访问成员首先需要创建Outer对象,所以就有了new Outer()写法了
Outer.Inner oi = new Outer().new Inner();
静态内部类可以直接通过类名.方法名进行创建,所以少了一个new关键字
三十二、对象克隆/拷贝
- 浅克隆、浅拷贝
上面一个是原对象,下面一个是浅克隆出来的对象。对于基本数据类型来说,直接把值拷贝过来即可;对于引用数据类型来说,是拷贝了引用的地址,两个数组的地址值完全相同,指向同一个内存地址。
- 深克隆、深拷贝
上面一个是原对象,下面一个是深克隆出来的对象。对于基本数据类型来说,直接把值拷贝过来即可;对于引用数据类型来说,如图上的数组类型,是在堆中新创建一个内容完全一样的数组,然后将地址返回给数组data;对于String字符串这种引用数据类型,由于创建字符串的时候是通过String username = “zhangsan”;的方式创建的,所以会复用字符串常量池中的地址。