Java基础

前言

环境配置和验证

KEYVALUE
JAVA_HOME是自己装 JDK 的路径
Path记录java.exe,javac.exe这些程序的位置(%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;)
CLASSPATHjava中运行的文件(xx.class)的位置(.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;)

验证:
        java
        javac
        java -version

1、语法基础

数据类型

java中数据类型分为基本数据类型(或者叫原生类、内置类型)和引用数据类型。

  • 基本数据类型包括:布尔型(boolean)字符型(char),整型(long,int,short,byte),浮点型(float,double)。
  • 引用数据类型包括:类(class),接口(interface),字符串(String),数组(Array)等等。引用数据类型变量存储的是对象的地址信息,也可以赋值为null。

基本数据同包装类的区别与联系

  • 基本数据类型存储在栈中,而包装类存储在堆中;传递方面:基本数据传递的数据,而包装类传递的是引用。
  • 包装类用于集合中的泛型使用,且用于支持入参及出参为空场景,但二者的转换存在装包拆包的过程。
  • 引用数据类型是不同数据类型的数据的集合,该集合是用来描述一类事物的封装体,引用数据类型变量,存储的是对象的地址信息也可以赋值为null。
基本数据类型字节数范围
byte1个字节(8位)-2^ 7~2^7-1
short2个字节(16位)-2^ 15~2^15-1
int4个字节(32位)-2^ 31~2^31-1
long8个字节(64位)-2^ 63~2^63-1
float4个字节(32位)-2^ 31~2^31-1
double8个字节(64位)-2^ 63~2^63-1
char2个字节(16位)-2^ 15~2^15-1
boolean1个字节(8位)-2^ 7~2^7-1

类型转换

(1)隐式类型转换

隐式转换也叫作自动类型转换,由系统自动完成,从存储范围小的类型到存储范围大的类型。

byte > short(char) > int > long > float > double

(2)显式类型转换(+= 内部含强制转换)

显示类型转换也叫作强制类型转换,是从存储范围大的类型到存储范围小的类型。我们需要将数值范围较大的数值类型赋给数值范围较小的数值类型变量时,由于此时可能会丢失精度。
在这里插入图片描述
类型转换测试

byte b1 = 20;
byte b2 = 30;
int result1 = b1;// 50 隐式类型转换 byte -> int
byte result2 = 30 + 20;// 50 无转换
// byte result3 = b1 + b2;// 编译报错,整型变量之间运算结果最小是int类型
byte result4 = (byte)(b1 + b2);// 50 显式类型转换也叫强转,int -> byte

ASCII码表

在这里插入图片描述

ASCII码表测试

char ch1 = 'a';
char ch2 = 'A';
char ch3 = 'y';
char ch4 = 'c';

System.out.println(ch1 + "=" + (int)ch1);// a=97
System.out.println(ch2 + "=" + (int)ch2);// A=65
System.out.println(ch3 + "=" + (int)ch3);// y=121
System.out.println(ch4 + "=" + (int)ch4);// c=99

进制的转换

  • 1开头为负数,0开头为正数。
  • 正数转化负数,取反加一,负数转化正数,取反加一。

(1)二进制转化成十进制:系数×权之和

01100110

系:0   1   1   0   0    1   1   0 	(从后往前)

权:1   2   4   8   16   32  64  128(2+4+32+64) 

十进制为102

(2)十进制转化成二进制

正数

122:二进制包含的权 64 32 16 8 2 

122二进制:01111010

负数

-59:59二进制包含的权 32 16 8 2 1

59二进制 0 0 1 1 1 0 1 1
取反     1 1 0 0 0 1 0 0
加一     1 1 0 0 0 1 0 1

-59二进制:01111010

(3)十进制转化成八进制

8进制:0 1 2 3 4 5 6 7,java中8进制的数以0开头
	
119(十进制) –>01110111(2进制)–>0167(8进制)3位一转换

(4)十进制转化成十六进制

16进制:1…9,a,b,c,d,e,f,java中16进制的数以0x开头	

119(十进制)–>01110111(2进制)–>0x77(16进制)4位一转换

进制测试

byte b1 = 20;// 十进制
byte b2 = 020;// 8进制
byte b3 = 0x20;// 16进制
System.out.println(b1);// 20
System.out.println(b2);// 16
System.out.println(b3);// 32
System.out.println(Integer.toBinaryString(119));// 1110111,119的二进制
System.out.println(Integer.toOctalString(119));// 77,119的8进制
System.out.println(Integer.toHexString(119));// 167,119的16进制

变量

用来存储数据的最小逻辑单元,在java中定义变量:数据类型 + 变量名。

类和变量的命名规范和命名规则

命名规范

  1. 见名知意。
  2. 类名单词首字母必须大写(如果是两个单词,每个单词首字母都要大写)。
  3. 变量首字母小写(如果是两个单词,第二个单词首字母大写)这种规范称为驼峰式命名规范。

命名规则

  1. java中的名字是由字母,数字,下划线,$等组成。
  2. 不能以数字开头。
  3. 不能是java中的关键字(例如:class,public,byte,int,long等)。

满足命名规则的的名字称为合法的标识符。

运算符

(1)算数运算符

加(+) 减(-) 乘(*) 除(/) 取余(%) 自增(++) 自减(- -)

(2)赋值运算符

= += -= *= /= %=

(3)关系运算符

大于(>) 小于(<) 大于等于(>=) 小于等于(<=) 不等于(!=) 等于(==)

(4)逻辑运算符

与(&) 或(|) 短路与(&&) 短路或(||) 异或(^)

  • 与的运算,有表达式是false,结果即为false,或的运算,有表达式是true,结果即为true。
  • 异或运算 相同为false,不同为true。

(5)三元运算符

布尔表达式?结果一:结果二

  • 当布尔表达式为true,返回结果一,当布尔表达式为false,返回结果二。

(6)移位运算符

左移(<<) 右移(>>) 无符号右移(>>>)

  • 左移动正数和负数都是低位补0,左移的应用场景:与2的n次幂乘法运算,使用左移动。
  • 右移正数高位补0,负数补1,正数右移,结果为除2的位次幂,正数除以2的n次幂。
  • 无符号右移正数和负数高位都补0。

运算符测试

// 演示++运算符
int i = 10;
i++;
System.out.println(i);// 11
int j = 30;
++j;
System.out.println(j);// 31
// 前++运算
int r1 = 50 + ++j;// 50+32
/* 前++执行顺序:
 * 1、执行 ++j
 * 2、执行 50+ ++j;
 * 3、将表达式结果赋值给r1
 */
System.out.println("r1=" + r1);// 82
System.out.println("j=" + j);// 32
// 后++运算
int r2 = 50 + j++;
/*
 * 后++执行顺序:
 * 1、执行50+j表达式的运算
 * 2、执行j++的运算
 * 3、将表达式的运算结果赋值给r2
 */
System.out.println("r2=" + r2);// 82
System.out.println("j=" + j);// 33
// 演示三元运算符 分页中最大页数的算法实现
// 每页显示的记录数
int perPage = 8;
// 总记录数
int records = 72;
// 计算最大页数
int maxPage = records % perPage == 0 ? records / perPage : records / perPage + 1;
System.out.println(maxPage);
// 演示移位运算
int i = 25;
int j1 = i << 2;// 100----(25*2^2)
System.out.println(j1);
int j2 = i >> 3;// j=3----(25/2^3)
System.out.println(j2);
int x1 = 25;
int x2 = -25;
int y1 = x1 >>> 3;
int y2 = x2 >>> 3;
System.out.println(y1);// 3
System.out.println(y2);// 536870908

2、语句基础

if语句

  • if语句通过判断逻辑决定执行哪些JAVA语句。
  • if(布尔表达式){语句体}。
  • if(布尔表达式){语句体}else{若干语句}。
  • if(布尔表达式){语句体}else if{语句体}else if{语句体}…else{语句体}。

switch语句

  1. case后常量值不能重复。
  2. case后常量值的类型与表达式结果的类型一致。
  3. 表达式结果的类型只能是byte,short,int,char,String,enum。
  4. 根据需求,选择是否使用break。
  5. default可以在任意位置。
  6. 可以有多个case常量值对应一组语句。

switch(表达式){
    case 常量值1:若干语句;break;
    case 常量值2:若干语句;break;
    case 常量值3:若干语句;break;
    …
    default:若干语句;
}

switch语法测试

int num = 2;
switch (num + 1) {
    case 1:
        System.out.println(num * num);
        break;
    case 2:
        System.out.println(num * num * num);
        break;
    case 3:
    case 5:
    case 6:
        System.out.println(num / 2);
        break;
    case 4:
        System.out.println(num / 4);
        break;
    default:
        System.out.println("over!");
}

// 结果 1

循环语句

for循环,while循环,do-while循环

  • while(布尔表达式){循环体}
  • do{循环体} while(布尔表达式),do-while循环,循环体至少执行一次。
  • continue:只能在循环中使用,结束被本次循环,继续下一个循环。
  • break:可以使用在循环体中,终止所在的循环体。

循环测试

for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= i; j++) {
        System.out.print(j + "*" + i + "=" + j * i + "  ");
    }
    System.out.println();
}

// 结果
// 1*1=1
// 1*2=2  2*2=4
// 1*3=3  2*3=6  3*3=9
// 1*4=4  2*4=8  3*4=12  4*4=16
// 1*5=5  2*5=10  3*5=15  4*5=20  5*5=25
// 1*6=6  2*6=12  3*6=18  4*6=24  5*6=30  6*6=36
// 1*7=7  2*7=14  3*7=21  4*7=28  5*7=35  6*7=42  7*7=49
// 1*8=8  2*8=16  3*8=24  4*8=32  5*8=40  6*8=48  7*8=56  8*8=64
// 1*9=9  2*9=18  3*9=27  4*9=36  5*9=45  6*9=54  7*9=63  8*9=72  9*9=81

3、数组

数组是相同数据类型的元素组成的集合,元素是按照一定顺序储存,数组长度一旦确定,是不能改变的。

定义数组的方式

  • int[] ages = {21, 27, 31, 19, 50, 32, 26, 25};
  • double[] sals = new double[5];
  • char[] codes = new char[] {‘0’, ‘1’, ‘2’, ‘a’, ‘b’, ‘c’};

数组,Java底层是如何给他分配内存的

在这里插入图片描述
在说堆和栈之前,我们先说一下JVM(虚拟机)内存的划分:

JVM内存的划分有五片:(1)寄存器(2)本地方法区(3)方法区(4)栈内存(5)堆内存

栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。

堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。

比如主函数里的语句 int [] arr=new int [3];在内存中是怎么被定义的:

主函数先进栈,在栈中定义一个变量arr,接下来为arr赋值,但是右边不是一个具体值,是一个实体。实体创建在堆里,在堆里首先通过new关键字开辟一个空间,内存在存储数据的时候都是通过地址来体现的,地址是一块连续的二进制,然后给这个实体分配一个内存地址。数组都是有一个索引,数组这个实体在堆内存中产生之后每一个空间都会进行默认的初始化(这是堆内存的特点,未初始化的数据是不能用的,但在堆里是可以用的,因为初始化过了,但是在栈里没有),不同的类型初始化的值不一样。所以堆和栈里就创建了变量和实体,我们刚刚说过给堆分配了一个地址,把堆的地址赋给arr,arr就通过地址指向了数组。所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它。这种我们不再叫他基本数据类型,而叫引用数据类型。称为arr引用了堆内存当中的实体。

比如主函数里的语句 int [] arr=null;在内存中是怎么被定义的:

arr不做任何指向,null的作用就是取消引用数据类型的指向。当一个实体,没有引用数据类型指向的时候,它在堆内存中不会被释放,而被当做一个垃圾,在不定时的时间内自动回收,因为Java有一个自动回收机制,(而c++没有,需要程序员手动回收,如果不回收就越堆越多,直到撑满内存溢出,所以Java在内存管理上优于c++)。自动回收机制(程序)自动监测堆里是否有垃圾,如果有,就会自动的做垃圾回收的动作,但是什么时候收不一定。

所以堆与栈的区别很明显:

  • 栈内存存储的是局部变量而堆内存存储的是实体。
  • 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短。
  • 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。

遍历(迭代)数组

   for(int a:数组名)
   {
       sum = sum + a;
   }

数组的扩容

  1. Arrays.copyOf(ages, ages.length+1)
  2. System.arraycopy(ages, 0, ages2, 0, ages.length)

演示数组的扩容

@Test
   public void test02() {
       int a[] = {1, 2, 3};
       int b[] = new int[5];
       System.arraycopy(a, 0, b, 0, a.length);
       System.out.println(Arrays.toString(b));
       int[] c = Arrays.copyOf(a, a.length);
       System.out.println(Arrays.toString(c));
   }
   
结果
[1, 2, 3, 0, 0]
[1, 2, 3]

数组排序

(1)API排序,Arrays类中Arrays.sort()

/**
  * API排序
  */
 @Test
 public void test03() {
     int b[] = {3, 1, 5};
     Arrays.sort(b);
     System.out.println(Arrays.toString(b));
 }

(2)冒泡排序(相邻相比)

/**
  * 冒泡排序
  */
 @Test
 public void test03() {
     int a[] = {3, 1, 4, 5, 2};
     int temp = 0;
     // 控制比较的轮数
     for (int i = 0; i < a.length - 1; i++) {
         // 每一轮比较的次数和操作
         for (int j = 0; j < a.length - i - 1; j++) {
             if (a[j] > a[j + 1]) {
                 temp = a[j];
                 a[j] = a[j + 1];
                 a[j + 1] = temp;
             }
         }
     }
     System.out.println(Arrays.toString(a));
 }

4、面向对象

如何定义类

java中使用class关键字定义类,类中可以定义该类的成员(成员变量、成员方法)。

例如:

public class Study04 {

class Person {
    int age;
    String name;
    double sal;

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

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

    public int getSum(int a, int b) {
        int sum = a * a + a * b;
        return sum;
    }

    public int getSum(int a) {
        int sum = a * a;
        return sum;
    }
}

构造方法

利用构造方法来创建对象,方法名和类名相同,没有返回值类型(不写void)。构造方法可以根据需求自行定义,如果不定义构造方法,系统会提供无参的构造方法如果定义了构造方法,系统将不再提供。

构造方法的分类(构造方法,它会优于其他方法执行)

  • 无参构造,只是用来创建对象。
  • 有参构造,不仅创建对象,对对象成员变量设置初始化值。

方法(函数)

方法是封装一组特定的实现逻辑功能代码的集合体,方法可以实现代码的重用。

方法的定义:[修饰符] 返回值类型 方法名([参数列表]){ 方法体; }

  • 定义方法需要确定方法的的返回值类型和参数即可
  • 确定功能最后是否有运算结果,如果有运算结果方法必须有返回值,否则没有返回值,如果有返回值,返回值的类型与运算结果的类型匹配。

演示方法,随机生成验证码

public static char[] generate() {
    char[] pool = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
        'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
    char[] codes = new char[4];
    Random r = new Random();
    for (int i = 0; i < 4; i++) {
        int index = r.nextInt(pool.length);
        codes[i] = pool[index];
    }
    return codes;
}

方法的重载

方法名相同,参数不同,称为方法的重载。

方法的签名:方法名和参数列表。

/**
 * 演示方法的重载
 */
@Test
public void test02() {
    Person p = new Person();
    System.out.println(p.getSum(1));
    System.out.println(p.getSum(1, 2));
}

如何创建对象、使用对象

演示面向对象

/**
 * 演示面向对象
 */
@Test
public void test01() {

    // 创建对象 java中使用new关键字创建对象
    // java中的类,系统会默认提供一个构造函数
    Person p1 = new Person();
    // 值为null的对象,是不可以访问属性和方法的,如果访问,运行期间会出现 NullPointerException异常,空指针异常
    Person p2 = null;
    // 使用对象 调用方法 对象通过.运算符访问类中的成员
    p1.study();
    p1.play();
    // 还可以使用对象访问类中的变量
    p1.age = 20;
    p1.name = "杨俊杰";
    p1.sal = 15000;
}

this关键字

用来访问当前对象的成员变量和成员方法,可以区分局部变量和成员变量。

(1)this是一个对象,在方法中使用的this,表示调用该方法的当前对象,this.age就是调用成员变量

public void study() {
    this.age = 1;
    System.out.println("Study...");
}

(2)this是一个对象,在方法中使用的this,表示调用该方法的当前对象,this.play()就是调用成员方法

public void study() {
    this.play();
    System.out.println("Study...");
}

(3)this可以调用其它构造函数,但是必须是构造函数中的第一条语句

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

public Person() {
    this(1, "1", 3);
};

对象

对象的内存管理,JVM开辟内存空间,将内存空间分为栈内存,堆内存,方法区,栈内存存储方法中的变量,堆内存中存储对象,方法区存储字节码内容。程序运行结束之后,栈区中局部变量空间会被自动释放,称方法出栈。程序运行结束后,堆内存的对象,当没有引用指向,jvm在某个时间点,会自动清除堆内存中的对象,释放内存,jvm实现垃圾回收可以手动调用System类中gc()方法;实现垃圾回收System.gc()。

成员变量、局部变量

成员变量生命周期:当创建对象new Cell(12,12)堆内存中为成员变量分配空间,当对象被垃圾回收,成员变量从堆内存消失。
局部变量生命周期:局部变量即为方法中定义的变量当方法被调用,局部变量进栈,当方法调用结束,栈区变量即出栈。

成员变量是定义在类中的,在使用之前可以不初始化,可以自动初始化值,类创建对象之后,成员变量自动进入堆内存中,成员变量在类内部都可以访问。
局部变量定义在方法中,不会自动初始化,成员变量在使用之前要定义赋值,当方法被调用的时候,局部变量会出现在栈内存中,局部变量只能在定义的方法内使用当方法执行结束,局部变量自动被清除。

方法区

方法区存储类的信息,jvm在运行一个java程序的时候,会将java程序字节码内容全部加载到方法区中,包括类中的方法,方法在内存中只有一份,如果通过一个类,创建多个对象,堆内存中会为每个对象分配空间,这些对象共用一个方法,创建多个对象,方法区类只记载一次。

5、继承

继承(extends)是实现类的继承的关键字

  • 子类(衍生类)会自动继承父类(超类)的非private成员变量和成员方法,子类 同时也可以自行定义成员变量和成员方法。
  • java中不支持多继承,一个子类只能有一个父类,一个父类可以有一个或多个子类。
  • java中所有的类都在一个继承体系内,java中object是所有类的父类,一个类如果没有指定父类默认继承object。

继承中的构造方法

  • 子类不继承父类的构造方法。
  • 子类中的构造方法总会调用父类无参构造方法。
  • super可以调用当前对象父类中的成员。
  • super调用父类的构造方法,必须是子类构造方法中第一条语句。
  • 所有子类的构造方法中,都会隐式的执行super()。
  • 如果父类中不提供无参构造器,子类中也没有使用supper调用父类构造器,会出现编译错误,解决的办法是类中定义有参构造,一定提供无参构造。

方法的重写

子类中,定义与父类方法名相同,参数相同的方法。

  • 子类重写父类的方法之后,创建子类对象无论是定义为父类型还是定义为子类型,调用重写的方法,总是执行子类方法。
  • 方法的重写,可以增强软件的可扩展性。
  • 通过继承和重写,实现业务方法的动态性(通用性)。

多态

引用类型的变量,定义为父类型,该变量就具备多态。

通过父类型的引用,调用父类型的方法,如果子类型重写了父类型的方法,父类型的引用指向了子类的对象,调用该方法就具备多态性。在程序中的多态,就是指同一个引用类型,使用不同的实例而执行不同的操作。

多态的意义:一个类型的引用指向不同的对象,会有不同的功能实现同一个对象,造型成不同的类型,会有不同的功能。

(1)向上造型,类型自动转化

一个类的对象可以向上造型为父类的类型和该类所实现的接口的类型,java编译器根据类型检查调用的方法是否匹配,运行的时候,是根据指向的具体对象决定调用哪个类中的方法。

(2)向下造型,强制转换

使用强制转换,必须确定该引用类型的变量指向的对象匹配要转换的类型,如果出现强转的类型与对象的真实类型不匹配不会出现编译错误,但是运行的时候会出现ClassCastException异常。

演示父类的引用指向子类的对象

/**
  * 父类的引用指向子类的对象此时只能调用父类的属性和方法play被重写,调用子类重写方法
  */
 @Test
 public void test01() {

     Person p = new Student();
     System.out.println(p.age + " " + p.name);
     p.study();
     p.play();
 }

结果
父类无参构造
子类无参构造
0 null
子类的study()
父类的play()

方法的重载和重写的区别

  1. 重载和重写是两种语法现象。
  2. 重载是在同一个类中,定义多个方法名相同,参数不同的方法,调用重载的方法,在编译期间,根据参数的个数或类型决定,重写是指子类中定义和父类完全相同的方法方法名和参数相同,调用重写的方法,根据对象的类型,并非根据引用的类型。
  3. 重载遵循的是编译器绑定,即在编译期间,根据方法的参数,决定绑定的参数,重写运行的是运行期绑定,即在运行期间,根据引用指向的具体对象决定绑定函数。

封装

java中提供了不同的封装级别:public、protected、默认的、private。

public:公共的,可以修饰类,成员变量,成员方法,修饰的成员在任何场景中都可以访问。

protected:受保护的,可以修饰成员变量和方法,修饰的成员在子类中可以访问,同一个包中可以访问。

默认:不加任何修饰符,类,变量,方法。

private:私有的,可以修饰成员变量和方法。

在这里插入图片描述

6、静态和常量

static 静态

(1)修饰变量

  • 静态变量,又称为类变量。
  • 静态变量不属于某个对象的数据结构。
  • 静态变量属于类的变量,可以通过类名来访问。
  • 静态变量和类的信息一起被存储到方法区,而不是在堆内存中。
  • 静态变量在内存中只有一份,被所有对象共享。

测试静态变量

/**
 * 演示静态变量
 */
class Cat {
    private int age;
    // 静态变量(类变量)
    public static int num = 12;

    public Cat(int age) {
        this.age = age;
        System.out.println(this.age + " " + Cat.num);
    }
}

public class Static_variable {

    public static void main(String[] args) {

        Cat cat1 = new Cat(4);// 4 12
        Cat.num = 20;
        Cat cat2 = new Cat(3);// 3 20

    }
}

(2)修饰方法

  • 静态方法,又称为类方法。
  • 定义格式 public static void fun(){} 或者 static public void fun(){}
  • 静态方法内只能直接访问静态变量和静态方法。
  • 静态方法内如果访问成员变量或成员方法必须创建对象,通过对象来访问。
  • 内存中,静态优于对象存在。

演示静态方法

/**
 * 演示静态方法
 */
public class Static_method {

    private int i1 = 20;
    private static int i2 = 30;

    public int getI1() {
        return i1;
    }

    public void setI1(int i1) {
        this.i1 = i1;
    }

    public static int getI2() {
        return i2;
    }

    public static void setI2(int i2) {
        Static_method.i2 = i2;
    }

    public void fun1() {
        System.out.println("我是一个非静态方法");
    }

    public static void fun2() {
        System.out.println("我是一个静态方法");
    }

    public static void fun3() {
        // 静态方法不能直接访问成员方法和成员变量
        // 如果访问,必须创建对象,由对象来访问
        // 可以直接访问静态变量和静态方法
        Static_method.i2 = 23;
        fun2();

        Static_method sm = new Static_method();
        sm.fun1();
        sm.i1 = 1;
    }

    public void fun4() {
        fun1();
        fun3();
    }
}

(3)静态代码块

  • 类中使用static修饰代码块。
  • 在类加载期间执行静态代码块,只执行一次。
  • 通常用来在软件中记载静态资源,例如加载数据库驱动程序。

(4)实例代码块(非静态代码块)

  • 每次创建对象之前,自动调用的代码块。

演示静态代码块和示例代码块

/**
 * 演示静态代码块 实例代码块
 */
public class Static_codeblock {

    public static void main(String[] args) {
        // 类加载,立即执行静态代码块,只执行一次
        // 每次创建对象之前,自动执行实例代码块
        Tool t1 = new Tool();
        t1.find();

        Tool t2 = new Tool();
        t2.find();

    }
}

class Tool {

    static {
        System.out.println("我是静态代码块");
    }

    {
        System.out.println("我是实例代码块");
    }

    public void find() {
        System.out.println("查询数据...");
    }
}

结果
我是静态代码块
我是实例代码块
查询数据...
我是实例代码块
查询数据...

final 常量

(1)修饰变量

final修饰的变量不能被改变了,称为常量。
       final int a=10;
       final int a;

final修饰成员变量两种初始化的方式

  1. 定义直接赋值

    public final int i=10;

  2. 构造方法初始化

    public final int i;
    public Foo()
    {
          i=10;
    }

final可以修饰局部变量,定义立即初始化即可,不初始化编译不报错但是无法使用
public void f(){
      final int h=10;
}

演示常量

/**
 * 演示final修饰变量
 */
public class Fanal {

    // 定义fanal变量可以同时初始化
    final int i = 10;
    // 可以在构造方法中,初始化final修饰的变量
    final int j;

    public Fanal() {
        j = 20;
    }

    public void fun() {
        final int y = 10;
        // final修饰的变量不能被改变,称为常量
        // i = 30;
    }

    public static void main(String[] args) {
        Fanal f = new Fanal();
        f.fun();
    }
}

(2)修饰方法

  • final的方法不能被重写。设计父类的方法不可以重写,防止子类定义的方法,改变父类方法的核心功能。

(3)修饰类

  • final的类不能被继承,JDK中很对的基础类都定义为final类,例如:String Integrt Double…
    定义final类意义在于保护父类,不会被继承修改。

演示常量修饰方法和类

/**
 * 演示final修饰的方法和类 final修饰的类不能被继承 final修饰的方法不能重写
 */
public class Final_Class_Method {
    public static void main(String[] args) {}
}

class F1 {
    public final void fun() {
        System.out.println("final fun()");
    }
}

class Son1 extends F1 {
    // 不可以重写父类中final方法
    // public void fun() {}
}

final class F2 {}

// 不能继承一个final类
// class Son2 extends F2{}

static final 静态常量

  • 静态常量必须定义的时候初始化
    public static final int i=10;
  • 静态常量在编译阶段,常量名直接被值替换。

演示静态常量

class CardList {
    public static final int THREE = 0;

    static final public int FOUR = 1;

    final static public int HREAT = 0;
}

7、抽象类和接口

抽象类和抽象方法

  • abstract用来定义抽象类和抽象方法的关键字。
  • 类中如果有实现的方法也有不能实现的方法,这样的类必须定义为抽象类。
  • 不能实现的方法通常定义为抽象方法。
  • 由abstract修饰的方法为抽象方法,抽象方法只有方法定义,没有方法体,使用分号结束。
  • 一个类中,如果有抽象方法,该类必须是抽象类,定义类必须使用abstart关键字。
  • 一个抽象类,可以没有抽象方法。
  • 抽象类不能创建对象,只能由子类创建对象,设计抽象类其实就是为了设计多个子类的共性功能。
  • 当类继承了抽象类,必须实现抽象类所有的抽象方法,如果子类也是抽象类,继承抽象类,可以不实现父类的抽象方法。
  • abstract和final不能同时修饰一个类

抽象类的意义:抽象类可以为多个子类提供提供一个公共的类型,可以封装子类中的重复内容(成员变量和方法),定义抽象方法,给子类提供一个统一的方法签名,用来制定标准、规范。

演示抽象类和抽象方法

/**
 * 演示:定义抽象类和抽象方法
 */
// 定义抽象类
abstract class Bird {
    // 定义抽象方法,没有方法体,使用分号结束
    public abstract void eat();

    public void fly() {}
}

// 继承抽象类,必须实现抽象类中的方法
class SomeBird extends OtherBird {
    @Override
    public void eat() {}
}

// 子类如果是抽象类,继承抽象类,可以不实现父类中的抽象方法
abstract class OtherBird extends Bird {}

接口

  • 接口是特殊的抽象类,接口中所有的方法都是抽象方法。

  • 定义接口使用interface关键字。

  • 接口中的方法都是public abstract的方法。

  • 定义可以不使用修饰符。

  • 接口中的成员变量都是静态常量。

  • 类既可以继承一个父类,同时也可以实现接口。

    例如:class Crile /extends Object/implements Shape,Serializable{}
    接口之间可以继承,而且可以多继承

抽象类和接口的区别

  1. 抽象类的定义 abstract class A{},接口的定义 interface A{}。
  2. 抽象类中可以有成员变量,接口只能有静态常量。
  3. 抽象类可以有抽象方法,也可以没有,接口中所有方法都是抽象方法。
  4. 抽象类只支持单继承,一个类可以实现多个接口,接口之间支持多继承。

演示接口

// 定义接口使用inerface关键字
interface Shape {
    // 接口中的成员变量都是静态常量
    public static final int I = 10;

    // 接口中的方法都是public抽象方法
    public abstract void area();
}

// 类如何实现接口,使用implements关键字实现接口
class Cirle extends Object implements Shape, Serializable {

    private static final long serialVersionUID = 1L;

    @Override
    public void area() {
        System.out.println("计算圆形的面积");

    }
}

// 接口之间可以多继承
interface A {}

interface B {}

interface C extends A, B {}

8、内部类

成员内部类

定义在类中的类,叫成员内部类。举例:

class Outer {
    private int time;

    class Inner {
        public void timeIn() {
            time++;
        }
    }

    public int getTime() {
        return time;
    }
}

如果想创建内部类对象,必须先创建外部类对象,因为在内部类中,有一个隐式的引用指向了创建它的外部类对象

// 创建内部类对象
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.timeIn();
System.out.println(outer.getTime());

结果
1

局部内部类

定义在方法中的内部类,叫局部内部类。

  • 局部内部类在使用外部成员的时候会报错,需要将外部成员使用final修饰。
class Outer2 {

    private int time;

    public void f(final int innerTime) {

        class Inner2 {
            public void fun() {
                // 局部内部类中访问局部变量,该变量必须是final的
                System.out.println(innerTime);
                time++;
            }
        }
        Inner2 inner = new Inner2();
        inner.fun();
    }

    public int getTime() {
        return time;
    }

    public void setTime(int time) {
        this.time = time;
    }
}

测试

Outer2 outer = new Outer2();
outer.f(100);
System.out.println(outer.getTime());

结果
100
1

匿名的内部类

匿名内部类中必须存在继承或实现。
演示匿名内部类

/**
 * 匿名内部类
 */
public class InnerClass_Ni {
    public static void main(String[] args) {
        new MyRunnable3() {
            @Override
            public void run() {
                System.out.println("匿名内部类:Run...");
            }
        }.run();;
    }
}

// 接口
abstract interface MyRunnable1 {

    public void run();
}

// 接口
interface MyRunnable2 {

    public void run();
}

// 抽象类
abstract class MyRunnable3 {

    public abstract void run();
}
/*
class MyRunnableImpl implements MyRunnable{
	@Override
	public void run() {
		System.out.println("Run...");
	}
}
*/

9、字符串

字符串的常量池

java.lang.String 表示字符串,final,不能被继承,字符串对象是不可变的,String常量池在方法区中。
jdk对字符串常量值只生成一个对象。

 @Test
 public void test01() {
     String str1 = "abc";
     String str2 = "abc";
     String str3 = new String("abc");
     String str4 = "ab" + "c";// jdk中,对字符串常量的并置操作进行优化,"ab"+"c"等价于"abc";
     String str5 = "ab";
     String str6 = str5 + "b";
     System.out.println(str1 == str2);// true
     System.out.println(str1 == str3);// false
     System.out.println(str1 == str4);// true
     System.out.println(str1 == str6);// false
 }

String的API

API描述
byte[] getBytes()内存中的存储方式
int length()字符长度
iint indexOf(String str)检索字符串,从0下标开始,不存在返回-1
int lastIndexOf(String str)检索字符串,从0下标开始访问远处那个,不存在返回-1
int indexOf(String str, int fromIndex)检索字符串,从哪个下标开始,不存在返回-1
int lastIndexOf(String str, int fromIndex)检索字符串,从0下标开始可以访问到哪里,不存在返回-1
String substring(int beginIndex, int endIndex)截取字符串从下标0开始,有范围
String substring(int beginIndex)截取字符串从下标0开始,到末尾
String trim()去前后空格
char charAt(int index)获取字符串中的一个字符,从0开始
boolean startsWith(String prefix)判断字符串的开头和结尾
String toUpperCase()字符串转大写
String toLowerCase()字符串转小写
String valueOf(int i)基本数据类型转换成String类型
String[] split(String regex)切割字符串
String replaceAll(String regex, String replacement)替换字符串

对API进行演示

/**
  * 内存中储存的方式 程序中,字符串中每个字符都算1个字符
  */
 @Test
 public void test01() {
     String str = "中国";
     // 内存中存储方式
     byte[] bytes = str.getBytes();
     System.out.println(Arrays.toString(bytes));// [-28, -72, -83, -27, -101, -67]
     // 程序中,字符串中每个字符都算1个字符
     System.out.println(str.length());// 2
 }

 /**
  * 检索字符串
  */
 @Test
 public void test02() {
     String str1 = "Hello Java";
     // 检索字符串,从0下标开始,不存在返回-1
     System.out.println(str1.indexOf("l"));// 2
     // 检索字符串,从0下标开始访问远处那个,不存在返回-1
     System.out.println(str1.lastIndexOf("l"));// 3
     // 检索字符串,从哪个下标开始,不存在返回-1
     System.out.println(str1.indexOf("el", 1));// 1
     // 检索字符串,从0下标开始可以访问到哪里,不存在返回-1
     System.out.println(str1.lastIndexOf("el", 0));// -1
 }

 /**
  * 截取字符串
  */
 @Test
 public void test03() {
     String str2 = "Hell Java";
     // 截取字符串从下标0开始,(2,5)
     System.out.println(str2.substring(2, 6));// ll J
     // 截取字符串从下标0开始,(2,结束)
     System.out.println(str2.substring(2));// ll Java
 }

 /**
  * 去前后空格
  */
 @Test
 public void test04() {
     String userName = " mygod ";
     // 去前后空格
     String newUserName = userName.trim();
     System.out.println(newUserName);// mygod
 }

 @Test
 public void test05() {
     String password = "123456";
     // 获取字符串中的一个字符,从0开始
     char c = password.charAt(3);
     System.out.println(c);// 4
 }

 /**
  * 判断字符串的开头和结尾
  */
 @Test
 public void test06() {
     String str = "Thinking in Java";
     // 判断字符串的开头
     boolean bo1 = str.startsWith("Th");
     System.out.println(bo1);// true
     // 判断字符串的结尾
     boolean bo2 = str.endsWith("Java");
     System.out.println(bo2);// true
 }

 /**
  * 大小写转换
  */
 @Test
 public void test07() {

     String str = "yangc";
     // 转大写
     String str1 = str.toUpperCase();
     System.out.println(str1);// YANGC
     // 转小写
     String str2 = str.toLowerCase();
     System.out.println(str2);// yangc
 }

 /**
  * 转换成String类型
  */
 @Test
 public void test08() {
     int num = 99782030;
     // 基本数据类型转换成String类型
     String strNum = String.valueOf(num);
     System.out.println(strNum);// 99782030
 }

 /**
  * 切割字符串
  */
 @Test
 public void test09() {
     String str = "12,23,34 56,97";
     // 将该字符串拆分为多个值为数字的字符串
     String[] numStrs = str.split(",");
     System.out.println(Arrays.toString(numStrs));// [12, 23, 34 56, 97]
 }

 /**
  * 切割字符串并且配合正则表达式
  */
 @Test
 public void test10() {
     String str = "12,23,34 56aa78b97";
     String[] numStrs = str.split(",|[a-zA-z]+|\\ ");
     System.out.println(Arrays.toString(numStrs));// [12, 23, 34, 56, 78, 97]
 }

 /**
  * 替换字符串
  */
 @Test
 public void test11() {
     String str = "aaa5bbb678ccc12ddd98";
     // 将如下字符串中的数字替换为"数字"
     String newStr = str.replaceAll("\\d+", "数字");
     System.out.println(newStr);// aaa数字bbb数字ccc数字ddd数字
 }

可变字符串StringBuilder

StringBuffer在为对象分配长度的时候,起始会分配一个字,也就是两个字节长度即(16位),每增加一个字符,长度就会在16的基础上加 1。

StringBuffer和StringBuilder功能完全相同,StringBuffer是线程安全的,所有的(api)方法,都是线程安全的,StringBuilder是非线程安全的,所有的方法都是非线程安全的。

API描述
int length()获取容器中字符数量(长度)
int capacity()获取该字符串容器默认容量
StringBuilder append(String str)向容器中追加字符串
String toString()将StringBuilder对象转换到String
int capacity()获取该字符串容器默认容量
StringBuilder insert(int offset, String str)某个位置插入内容
StringBuilder delete(int start, int end)删除某个位置内容
StringBuilder replace(int start, int end, String str)替换某个位置内容
StringBuilder reverse()倒置字符

测试API

/**
  * 测试容量和字符长度
  */
 @Test
 public void test01() {
     StringBuilder sb = new StringBuilder("Hello");
     // 获取容器中字符的数量
     System.out.println(sb.length());// 5
     // 获取该字符串容器默认容量
     System.out.println(sb.capacity());// 21
 }

 /**
  * 演示StringBuilder维护的是一个可变的字符串
  */
 @Test
 public void test02() {
     StringBuilder sb = new StringBuilder("Hello");

     // 向容器中追加字符串
     StringBuilder sb2 = sb.append(" Java");
     System.out.println(sb == sb2);// true

     // 将StringBuilder对象转换到String
     System.out.println(sb.toString());// Hello Java
     System.out.println(sb2.toString());// Hello Java

     // append()方法 return this;
     sb.append("a").append("b").append("c");
     sb.append(true);
     sb.append(10);
     System.out.println(sb);// Hello Javaabctrue10
 }

 /**
  * 向StringBuilder中插入内容、删除字符、替换字符
  */
 @Test
 public void test03() {
     StringBuilder builder = new StringBuilder("java");

     // 某个位置插入内容
     builder.insert(4, "hahaha");
     System.out.println(builder.toString());// javahahaha

     // 删除某个位置内容,删除4-6
     builder.delete(4, 7);
     System.out.println(builder);// javaaha

     // 替换某个位置内容,替换0-3
     builder.replace(0, 4, "C#");// C#aha
     System.out.println(builder);

     // 倒置字符
     builder.reverse();
     System.out.println(builder);// aha#C
 }

中文乱码的现象

// 浏览器
String name = "张三";
// 演示浏览器使用GBK编码表 UTF-8
// 编码 将字符串->码值
byte[] bytes = name.getBytes("UTF-8");
System.out.println(Arrays.toString(bytes));// [-27, -68, -96, -28, -72, -119]
// 服务器(软件)
// 解码 将码值->字符串
String sname = new String(bytes, "ISO-8859-1");
System.out.println(sname);// å¼ ä¸‰

// 程序员处理中文乱码
// 获取到浏览器发送的码值
byte[] brbytes = sname.getBytes("ISO-8859-1");
System.out.println(brbytes);// [B@34340fab
String rname = new String(brbytes, "UTF-8");
System.out.println(rname);// 张三

10、正则表达式

用来检验字符串的格式,采用独特的语法描述字符的某种组合规则。字符串类中,提供了API支持使用正则表达式。

String 类中支持正则API

bolean matches(String reg);判断字符是否匹配规则

String[] split(String reg);拆分字符串

String replaceAll(String reg,String con);替换某个字符串中的内容
符号描述
^表示匹配行首的文本(以谁开始)
$表示匹配行尾的文本(以谁结束)
*0位或多位
+1位或多位
?0位或一位
{n}n位
\转义字符
\d用于匹配从0到9的数字 [0-9]
\D用于匹配从0到9以外的字符 [^0-9]
\w用于匹配字母,数字或下划线字符 [A-Za-z0-9]
\W用于匹配字母,数字或下划线以外的字符 [^A-Za-z0-9]
\s用于匹配单个空格符,包括tab键和换行符
\S用于匹配除单个空格符之外的所有字符
()分组
&&与的关系
或的关系
.用于匹配除换行符之外的所有字符

部分演示

/**
 * 匹配数字
 */
@Test
public void test01() {
    String str = "024";
    String reg = "^[0123456789][0123456789][0123456789]$";
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

/**
 * 匹配数字
 */
@Test
public void test02() {
    String str = "024";
    String reg = "^[0-9][0-9][0-9]$";
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

/**
 * 匹配字母和数字
 */
@Test
public void test03() {
    String str = "Z24";
    String reg = "^[a-zA-Z][0-9][0-9]$";
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

/**
 * 匹配数字 {}表示范围
 */
@Test
public void test04() {
    String str = "2445";
    String reg = "^[0-9]{3,6}$";// 3-6位
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

/**
 * 转义字符 \"->" \u0000
 */
@Test
public void test05() {
    System.out.println("\"");// "
    System.out.println("\\");// \
    System.out.println("\'");// '
}

/**
 * \d数字
 */
@Test
public void test06() {
    String str = "2445";
    String reg = "^\\d{3,6}$";
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

/**
 * 测试分组和通配符 验证带有国家区号的手机号
 */
@Test
public void test07() {
    String str = "15699782030";
    String reg = "^(\\+086|\\+0086)?\\d{11}$";
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

// 测试分组和通配符
// 验证带有国家区号的手机号
// [0-9&&[^0-2]]:0-9不包含0-2
// 正则中&&表示与的关系
// 正则中|表示或的关系
@Test
public void test08() {
    String str = "+008613699782030";
    String reg = "^(\\+086|\\+0086)?1[0-9&&[^0-2]]\\d{9}$";
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

/**
 * \s:用于匹配单个空格符,包括tab键和换行符 \S:用于匹配除单个空格符之外的所有字符 \d:用于匹配从0到9的数字 \w:用于匹配字母,数字或下划线字符 \W:用于匹配所有与\w不匹配的字符 .
 * :用于匹配除换行符之外的所有字符
 */
@Test
public void test09() {
    String str = "luchong@tedu.cn";
    String reg = "^\\w+@\\w+\\.\\w+$";
    boolean bo = str.matches(reg);
    System.out.println(bo);// true
}

11、Object

JDK中所有的API都在同一个继承体系内,Object位于体系中的最顶层,继承java.lang.Object类, Object类型的引用可以指向所有类型的对象。

(1)== 比较

== : 基本数据类型 == 比较的值,引用数据类型 == 比较的是内存地址。

(2)equals 比较

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  1. 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过 “==” 比较这两个对象。
  2. 类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

(3)说明

String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals方法比较的是对象的值。

(4)equals 和 hashcode 的关系
在这里插入图片描述
上图的 1 2 3 4 5 就好比 hashcode,A B C D 就好比 equals,关系如下

  • 两个对象相等,hashcode一定相等

  • 两个对象不等,hashcode不一定不等

  • hashcode相等,两个对象不一定相等

  • hashcode不等,两个对象一定不等

测试重写 equals 和 hashcode

 @Test
    public void test() {
        Cell cell1 = new Cell(5, 6);
        Cell cell2 = new Cell(5, 6);
        System.out.println(cell1 == cell2);// false
        System.out.println(cell1.equals(cell2));// true
        System.out.println(cell1.hashCode() == cell2.hashCode());// true
    }
}

class Cell {
    private int row;
    private int col;

    public Cell(int row, int col) {
        super();
        this.row = row;
        this.col = col;
    }

    @Override
    public int hashCode() {
        return Objects.hash(row, col);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Cell other = (Cell)obj;
        if (col != other.col) {
            return false;
        }
        if (row != other.row) {
            return false;
        }
        return true;
    }
}

12、包装类

包装类型基本类型
Integerint
Bytebyte
Shortshort
Longlong
Doubledouble
Floatfloat
Booleanboolean
Characterchar

演示包装类部分API

/**
  * 测试Number,整型和浮点型包装类共同的父亲是Number抽象类
  */
 @Test
 public void test01() {
     Number intObj = new Integer(10);
     int i = intObj.intValue();
     System.out.println(i);// 10
 }

 /**
  * 测试Integer
  */
 @Test
 public void test02() {
     String str = "200";
     int money = Integer.parseInt(str);
     System.out.println(money);// 200
     // 如果字符串不满足整型格式,程序会抛出NumberFormatException
     String input = "两百";
     int inputMoney = Integer.parseInt(input);
     System.out.println(inputMoney);// java.lang.NumberFormatException: For input string: "两百"
 }

 /**
  * 测试Double
  */
 @Test
 public void test03() {
     String str = "2345.67";
     Double money = Double.parseDouble(str);
     double inputMoney = money.doubleValue();
     System.out.println(inputMoney);// 2345.67
 }

 /**
  * 自动装箱、自动拆箱
  */
 @Test
 public void test04() {
     // 自动装箱
     Integer i1 = 10;
     // 自动拆箱
     int i2 = i1;
 }	 

13、时间和日期

/**
 * 测试Date
 */
@Test
public void test01() {
    // 当前系统时间
    Date date = new Date();
    System.out.println(date);// Wed Jun 08 14:05:11 GMT+08:00 2022
    // 获取当前时间的毫秒数 距1970-1-1时间点的好秒数
    long time = date.getTime();
    System.out.println(time);// 1654668311649
    // 设置距1970-1-1时间点的好秒数,获取时间
    date.setTime(10000);
    System.out.println(date);// Thu Jan 01 08:00:10 GMT+08:00 1970
}

/**
 * 字符串与Date之间的转换 SimpleDataFormat:用来实现String与Date之间的转换 将字符串转换到日期对象
 */
@Test
public void test02() throws ParseException {
    String str = "2019-8-16 09:35:00";
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 字符串->日期
    Date date = sdf.parse(str);
    System.out.println(date);// Fri Aug 16 09:35:00 GMT+08:00 2019
}

/**
 * 测试将日期对象转换到特定的日期格式字符串 格式:星期 年-月-日 时:分:秒
 */
@Test
public void test03() {
    Date date = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat("E yyyy年MM-dd HH:mm:ss");
    String dateStr = sdf.format(date);
    System.out.println(dateStr);// 周三 2022年06-08 14:06:50
}

/**
 * Calendar,封装一个完整的日历,提供对日期中所有字段操作
 */
@Test
public void test05() {
    Calendar c = Calendar.getInstance();
    System.out.println(c);// java.util.GregorianCalendar[time=1654668468736,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=5,WEEK_OF_YEAR=24,WEEK_OF_MONTH=2,DAY_OF_MONTH=8,DAY_OF_YEAR=159,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=2,HOUR_OF_DAY=14,MINUTE=7,SECOND=48,MILLISECOND=736,ZONE_OFFSET=28800000,DST_OFFSET=0]

    // Calendar->Date
    Date date1 = c.getTime();
    System.out.println(date1);// Wed Jun 08 14:07:48 GMT+08:00 2022

    // Date->Calendar
    Date date2 = new Date();
    System.out.println(date2);// Wed Jun 08 14:07:48 GMT+08:00 2022

    Calendar c2 = Calendar.getInstance();
    c2.setTime(date2);
    System.out.println(c2);// java.util.GregorianCalendar[time=1654668579767,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=5,WEEK_OF_YEAR=24,WEEK_OF_MONTH=2,DAY_OF_MONTH=8,DAY_OF_YEAR=159,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=2,HOUR_OF_DAY=14,MINUTE=9,SECOND=39,MILLISECOND=767,ZONE_OFFSET=28800000,DST_OFFSET=0]
    // 获取年
    System.out.println(c2.get(Calendar.YEAR));// 2022
    // 获取月
    System.out.println(c2.get(Calendar.MONTH) + 1);// 6
}

/**
 * Calendar设置日期中的字段值
 */
@Test
public void test06() {
    Calendar c = Calendar.getInstance();
    c.set(Calendar.YEAR, 2019);
    c.set(Calendar.MONTH, Calendar.AUGUST);
    c.set(Calendar.DAY_OF_MONTH, 16);
    c.set(Calendar.HOUR, 10);
    System.out.println(c.getTime());// Fri Aug 16 22:13:29 GMT+08:00 2019
}

/**
 * Calendar获取日期中时间字段
 */
@Test
public void test07() {
    Calendar c = Calendar.getInstance();
    Date date = c.getTime();
    System.out.println(date);// Wed Jun 08 14:16:23 GMT+08:00 2022
    System.out.println(c.get(Calendar.YEAR));// 2022
    System.out.println(c.get(Calendar.MONTH) + 1 + "月");// 6月
    System.out.println("星期" + (c.get(Calendar.DAY_OF_WEEK) - 1));// 星期3
    System.out.println(c.get(Calendar.DAY_OF_MONTH));// 8
}

/**
 * 测试add(int field,int num)方法
 */
@Test
public void test08() {
    Calendar c = Calendar.getInstance();
    // 3天后生效
    c.add(Calendar.DAY_OF_MONTH, 3);
    System.out.println(c.getTime());// Sat Jun 11 14:17:30 GMT+08:00 2022
    // 1个月后生效
    Calendar c1 = Calendar.getInstance();
    c1.add(Calendar.MONTH, 1);
    System.out.println(c1.getTime());// Fri Jul 08 14:17:30 GMT+08:00 2022
    // 1个月之前
    Calendar c2 = Calendar.getInstance();
    c2.add(Calendar.MONTH, -1);
    System.out.println(c2.getTime());// Sun May 08 14:17:30 GMT+08:00 2022
}

/**
 * 测试java.util.Date<->java.sql.Date
 */
@Test
public void test09() {
    java.sql.Date sqlDate = new java.sql.Date(10000);

    System.out.println(sqlDate);// 1970-01-01
    // 1970月01月01 字符串 SimpleDateFormat
    // java.util.Date
    java.util.Date utilDate = new java.util.Date();
    // java.sql.Date是java.util.Date的子类
    utilDate = sqlDate;
    // java.util.Date->java.sql.Date
    java.sql.Date nowDate = new java.sql.Date(utilDate.getTime());
    System.out.println(nowDate);// 1970-01-01
}

14、集合

用来管理多个对象特殊的数据结构的容器,JDK提供了多个这样的容器,每个容器都有特定的数据结构,JDK将这些容器通过层次关系管理。

Collection接口定义了所有的集合API的共性功能,常用API

方法描述
void add(Object obj)添加对象
boolean contains(Object obj)使用对象的equals()判断集合中是否包含某个对象
int size()获取集合中对象数量
void clear()清除集合中的对象
boolean isEmpty()判断集合是否为空
boolean addAll(Collection c2)向集合添加其他集合,原集合发生变化返回true
boolean containsAll(Collection c2)判断一个集合是否包含另一个集合
boolean remove(Object o)移除元素

测试API

/**
 * 测试Collection接口添加对象的方法 集合中存储的是对象的引用,而不是对象
 */
@Test
public void test01() {
    Collection<Cell> cells = new ArrayList<Cell>();
    Cell cell1 = new Cell(5, 6);
    Cell cell2 = new Cell(6, 5);
    Cell cell3 = new Cell(5, 6);
    // 将对象添加到集合中
    cells.add(cell1);
    cells.add(cell2);
    cells.add(cell3);
    System.out.println(cells);// [Cell [row=5, col=6], Cell [row=6, col=5], Cell [row=5, col=6]]
}

/**
 * 测试 boolean contains()方法 通过对多个对象做equals()比较
 */
@Test
public void test02() {
    Cell cell1 = new Cell(5, 6);
    Cell cell2 = new Cell(15, 16);
    Collection<Cell> cells = new ArrayList<Cell>();
    cells.add(cell1);
    cells.add(cell2);
    Cell cell3 = new Cell(5, 6);
    boolean result = cells.contains(cell3);
    System.out.println(result);// true
}

/**
 * 测试size();获取集合中对象的数量 测试clear();清空集合中的对象 测试isEmpty();判断集合是否是空
 */
@Test
public void test03() {

    Cell cell1 = new Cell(1, 2);
    Cell cell2 = new Cell(1, 2);
    Cell cell3 = new Cell(3, 4);
    Cell cell4 = new Cell(5, 6);

    Collection<Cell> cells = new ArrayList<Cell>();
    cells.add(cell1);
    cells.add(cell2);
    cells.add(cell3);
    cells.add(cell4);

    int size = cells.size();
    System.out.println(size);// 4
    System.out.println(cells.isEmpty());// false

    cells.clear();// 清空
    System.out.println(cells.size());// 0
    System.out.println(cells.isEmpty());// true
}

/**
 * 测试addAll()
 */
@Test
public void test04() {
    Collection<String> c1 = new ArrayList<String>();
    c1.add("JavaScript");
    c1.add("SQL");
    c1.add("Java");
    c1.add("HTML");
    Collection<String> c2 = new ArrayList<String>();
    c2.add("CSS");
    c2.add("JSP");
    c2.add("Servlet");
    // 如果集合发生变化返回true,否则返回false
    boolean bo = c1.addAll(c2);
    System.out.println(c1);// [JavaScript, SQL, Java, HTML, CSS, JSP, Servlet]
    System.out.println(bo);// true
}

/**
 1. 测试containsAll()
 */
@Test
public void test05() {
    Collection<String> c1 = new ArrayList<String>();
    c1.add("java");
    c1.add("SQL");
    Collection<String> c2 = new ArrayList<String>();
    c2.add("Html");
    c2.add("css");
    c2.add("javaScript");
    c1.add("Html");
    c1.add("css");
    c1.add("javaScript");
    boolean bo = c1.containsAll(c2);
    System.out.println(bo);// true
}		

集合的迭代

  1. 增强for循环

    for(String str:c1)
    {
         System.out.println(str);
    }

  2. 迭代器迭代

迭代器API描述
Iterator iterator()返回迭代器对象
boolean hasNext()判断容器中是否还有对象
Object next()迭代对象

测试

/**
 * 增强的for循环,只能用来迭代数组和集合 当迭代集合的时候,内部采用的是迭代器的方式 编译期间,编译器将增强的for循环转换为 迭代器迭代数据的字节码
  */
 @Test
 public void test01() {
     Collection<String> c1 = new HashSet<String>();
     c1.add("java");
     c1.add("css");
     c1.add("html");
     c1.add("javaScript");
     for (String str : c1) {
         System.out.println(str);
     }
 }

 /**
 * 测试Collection迭代对象的方式 迭代器的方式 Iterator接口,定义了迭代Collection 容器中对象的统一操作方式 集合对象中的迭代器是采用内部类的方式实现 这些内部类都实现了Iterator接口
 * 使用迭代器迭代集合中数据期间,不能使用集合对象 删除集合中的数据
  */
 @Test
 public void test02() {

     Collection<String> c1 = new HashSet<String>();
     c1.add("java");
     c1.add("css");
     c1.add("html");
     c1.add("javaScript");
     Iterator<String> it = c1.iterator();
     while (it.hasNext()) {
         String str = it.next();
         System.out.println(str);// css java javaScript html
         if (str.equals("css")) {
             // c1.remove(str);//会抛出异常
             it.remove();
         }
     }
     System.out.println(c1);// [java, javaScript, html]
 }

List集合

  • 元素有序,可以重复,第一个元素的索引值为0。
  • ArrayList(数组结构)算法+对象数组,数据结构即为数组的数据结构,该长度是长度可变的数组,查询效率高,更新效率低。
  • LinkedList(链表结构)对象之间存在一个对方的引用 查询对象,通过整个链表依次查找,获取对象查询效率比ArrayList低,更新效率比他高。
API描述
boolean add(E e)添加元素
void add(int index, E element)指定位置添加元素
public E removeFirst()移除链表头
public void addFirst(E e)添加链表头

测试

/**
  * 测试list中可以添加相同的对象
  */
 @Test
 public void test01() {
     List<String> list = new ArrayList<String>();
     list.add("Java");
     list.add("Java");
     list.add("Html");
     list.add("Css");
     System.out.println(list.size());// 4
 }

 /**
  * 测试list中元素是有序的,第一个元素的索引值为0
  */
 @Test
 public void test02() {
     List<String> list = new ArrayList<String>();
     list.add("Java");
     list.add("Html");
     list.add(1, "Css");// 1是下标
     list.add("Java");
     System.out.println(list);// [Java, Css, Html, Java]
 }

 /**
  * LinkedList addFirst 添加头 removeFirst 移除头
  */
 @Test
 public void test03() {
     List<String> list = new LinkedList<String>();
     list.add("Java");
     list.add("Java");
     list.add("Html");
     list.add("Css");

     ((LinkedList<String>)list).removeFirst();
     ((LinkedList<String>)list).addFirst("yang");

     for (String c : list) {
         System.out.println(c);
     }
 }

(1)迭代

三种迭代方式,分别是根据索引迭代,增强FOR循环迭代和迭代器迭代。

测试

/**
 * 测试迭代List集合
 */
@Test
public void test04() {
    List<String> list = new ArrayList<String>();
    list.add("Java");
    list.add("Java");
    list.add("Html");
    list.add("Css");

    System.out.println("----根据索引迭代----");
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    System.out.println("----迭代器迭代----");
    for (Iterator it = list.iterator(); it.hasNext();) {
        String c = (String)it.next();
        System.out.println(c);
    }
    System.out.println("----增强For循环迭代----");
    for (String c : list) {
        System.out.println(c);
    }
}

/**
 * 测试ArrayList 查询快 更新慢
 */
@Test
public void test05() {
    List<String> list = new ArrayList<String>();
    list.add("Java");
    list.add("Java");
    list.add("Html");
    list.add("Css");
    System.out.println("------删除后迭代-----");
    list.remove(2);
    for (String c : list) {
        System.out.println(c);
    }
}

(2)数组和集合相互转换

集合转到数组使用 toArray 方法,但是当我们使用数组转到集合的时候就有一些情况需要分下,首先分为基础数据类型数组和引用数据类型数组转集合

A.基础数据类型数组

  • 当把基础数据类型的数组转为集合时,实际上是将这个数组存入了Arrays的内部类ArrayList中的E[]数组中,如果想基本类型也可以实现转换(依赖boxed的装箱操作)。

B.引用对象类型数组

  • 使用基础数据类型的包装类或其他引用对象数组,传入到内部类的就是数组中的元素, 数组转换到集合之后,不能对集合中的对象操作, 如果想操作转换后的集合,需要添加到一个新的集合中。

测试

/**
  * 测试集合转换到数组
  */
 @Test
 public void test01() {
     List<String> list = new ArrayList<String>();
     list.add("a");
     list.add("b");
     list.add("c");
     String[] strArray = list.toArray(new String[] {});
     System.out.println(Arrays.toString(strArray));// [a, b, c]
 }

 /**
  * 测试数组转换到集合,数组转换到集合之后,不能对集合中的对象操作,如果想操作转换后的集合,需要添加到一个新的集合中
  */
 @Test
 public void test02() {
     /**
      * A.基本数据类型数组 当把基础数据类型的数组转为集合时,实际上是将这个数组存入了Arrays的内部类ArrayList中的E[]数组中
      */
     int[] intArr = {1, 2, 3};
     List<int[]> intList = Arrays.asList(intArr);// 可以看到元素类型是数组,明显不对
     System.out.println(intList);// [[I@3f8f9dd6]

     // 如果想基本类型也可以实现转换(依赖boxed的装箱操作)
     List intList1 = Arrays.stream(intArr).boxed().collect(Collectors.toList());
     intList1.add(5);
     System.out.println(intList1);// [1, 2, 3, 5]

     /**
      * B.引用数据类型数组 使用基础数据类型的包装类或其他引用对象数组,传入到内部类的就是数组中的元素,数组转换到集合之后不能对集合中的对象操作 如果想操作转换后的集合,需要添加到一个新的集合中
      */
     String[] strArr = {"a", "b", "c"};
     // 方式一
     List<String> list = Arrays.asList(strArr);
     // 方式二
     // List<String> list = Arrays.stream(strArr).collect(Collectors.toList());

     // list.add("d");会抛出异常
     List<String> newList = new ArrayList<String>();
     newList.addAll(list);
     newList.add("d");
     System.out.println(newList);// [a, b, c, d]
 }

(3)排序

  1. Collections类,是操作集合的工具类,Collections.sort(list)方法进行排序,使用此方法排序集合元素类型必须实现Comparable接口重写compareTo()方法,否则无法实现排序。
  2. Comparator接口,制定对某个对象的,排序规则,重写compare()方法,使用匿名内部类。
Collections.sort(list,new Comparator<Person>() {
    @Override
    public int compare(Person o1, Person o2) {
        return o1.getAge()-o2.getAge();
    }
});

测试

/**
  * Comparable接口 集合对象或Collections排序API对元素排序的时候,对象必须实现Comparable接口,否则无法实现排序
  * compareTo()方法,Comparable接口中的方法,集合对象或Collections排序API调用的方法,将对象的排序规则在compareTo()方法中实现
  */
 @Test
 public void test04() {
     List<Cell> cells = new ArrayList<Cell>();
     Random r = new Random();
     for (int i = 0; i < 3; i++) {
         int row = r.nextInt(50);
         int col = r.nextInt(50);
         cells.add(new Cell(row, col));
     }
     for (Cell c : cells) {
         System.out.println(c);// Cell{row=28, col=0} Cell{row=15, col=46} Cell{row=31, col=3}
     }
     Collections.sort(cells);
     System.out.println("排序后:");
     for (Cell c : cells) {
         System.out.println(c);// Cell{row=15, col=46} Cell{row=28, col=0} Cell{row=31, col=3}
     }
 }

 /**
  * 测试Comparator接口,制定对某个对象的 排序规则,重写compare()方法
  */
 @Test
 public void test05() {
     List<Person> list = new ArrayList<Person>();
     Random r = new Random();
     for (int i = 0; i < 3; i++) {
         int age = r.nextInt(70);
         list.add(new Person(1001 + i, age, "p" + i));
     }
     for (Person p : list) {
         System.out.println(p);
     }
     // 此功能中需要根据Person的年龄排序
     // 排序后
     System.out.println("排序后");
     // Collections.sort(list,new HH());
     // 匿名内部类
     Collections.sort(list, new Comparator<Person>() {
         @Override
         public int compare(Person o1, Person o2) {
             return o1.getAge() - o2.getAge();
         }
     });
     for (Person p : list) {
         System.out.println(p);
     }
 }

(4)扩容

ArrayList无参构造

//Object类型的数组 elmentData []
transient Object[] elementData;
//{}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList无参构造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

底层用的是一个Object类型的数组elementData,当使用无参构造方法ArrayList后elementData是空的,也就是说使用无参构造方法后容量为0。

//容量为10
private static final int DEFAULT_CAPACITY = 10;
//add添加元素方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}
//所需最小容量方法
private void ensureCapacityInternal(int minCapacity) {
	//空数组初始所需最小容量为10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
//是否需要扩容方法
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    //所需最小容量当前数组能否存下,如果现在数组存不下进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
//容器扩容方法
private void grow(int minCapacity) {
    //旧容量(原数组的长度)
    int oldCapacity = elementData.length;
    //新容量(旧容量加上旧容量右移一位,也就是1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果计算出的新容量比最小所需容量小就用最小所需容量作为新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //如果计算出的新容量比MAX_ARRAY_SIZE大, 就调用hugeCapacity计算新容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //数组扩容成新容量
    elementData = Arrays.copyOf(elementData, newCapacity);
}

由此看出只有当第一次add添加元素的时候,才初始化容量,因为是空数组所需的最小容量为10,而 elementData 大小为0,新容量算出类也是0,此时最小所需容量作为新容量为10。

例如:

ArrayList<Object> objects = new ArrayList<>();
长度:  1 容量: 10
长度:  5 容量: 10
长度:  11 容量: 15
长度:  15 容量: 15
长度:  21 容量: 22

ArrayList有参构造

//有参构造方法
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

有参构造和无参构造区别就是给数组初始化了长度initialCapacity并且数组不为空,不为空的数组最小所需容量就是集合元素长度,集合元素长度超过初始化长度initialCapacity值才扩容,扩容逻辑和无参构造一致。

例如:

ArrayList<Object> objects = new ArrayList<>(5);
长度:  3 容量: 5
长度:  5 容量: 5
长度:  7 容量: 7
长度:  11 容量: 15
长度:  15 容量: 15
长度:  19 容量: 22
ArrayList<Object> objects = new ArrayList<>(13);
长度:  15 容量: 19
长度:  17 容量: 19
长度:  21 容量: 28
长度:  25 容量: 28
长度:  29 容量: 42

Vector 扩容机制

Vector 的底层也是一个数组 elmentData ,但相对于 ArrayList 来说,它是线程安全的,它的每个操作方法都是加了锁的。如果在开发中需要保证线程安全,则可以使用 Vector。扩容机制也与 ArrayList 大致相同。唯一需要注意的一点是,Vector 的扩容量是2倍。

结论

数据类型底层数据结构默认初始容量加载因子扩容增量
ArrayList数组10(jdk7)0(jdk8)加载因子1(元素满了扩容)0.5:扩容后容量为原容量的1.5倍
Vector数组10加载因子1(元素满了扩容)1:扩容后容量为原容量的2倍

LinkedList,链表结构,且是是双向链表,不涉及扩容的问题。

Set集合

HashSet

  1. 底层数据结构为链表的数组(哈希表)。
  2. 无序,Set不能添加相同的对象,Set集合调用对象的equals()和hashCode()方法判断是否是一个对象哈希表底层依赖两个方法: hashCode()和equals() 执行顺序首先比较哈希值是否相同。

         相同: 继续执行equals()方法返回true为元素重复了,不添加,返回false直接把元素添加到集合。
         不同:就直接把元素添加到集合。

TreeSet

  1. 底层数据结构是红黑树(是一个自平衡的二叉树)。
  2. 保证元素的排序方式。
  3. TreeSet中的对象必须实现Comparable接口,HashSet的性能总是比TreeSet好(特别是最常用的添加,查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。

测试

/**
  * 测试Set不能添加相同的对象
  */
 @Test
 public void test01() {
     Set<String> cells = new HashSet<String>();
     cells.add("a");
     cells.add("b");
     cells.add("a");
     System.out.println(cells.size());// 2
 }

 /**
  * 测试Set中元素无序,添加的顺序和取出的顺序不一致
  */
 @Test
 public void test02() {
     Set<String> cells = new HashSet<String>();
     cells.add("a");
     cells.add("d");
     cells.add("b");
     cells.add("a");
     cells.add("c");
     // 迭代Set集合
     Iterator<String> it = cells.iterator();
     while (it.hasNext()) {
         System.out.println(it.next());// a b c d
     }
 }

 /**
  * TreeSet中的对象必须实现Comparable接口
  */
 @Test
 public void test03() {
     Set<String> cells = new HashSet<String>();
     cells.add("a");
     cells.add("d");
     cells.add("b");
     cells.add("a");
     cells.add("c");
     for (String c : cells) {
         System.out.println(c);// a b c d
     }
 }

Map集合

HashMap,底层是哈希表数据结构,线程是不同步的,可以存入null键,null值,要保证键的唯一性,需要覆盖hashCode方法,和equals方法。

Hashtable, 底层是哈希表数据结构,线程是同步的,不可以存入null键,null值,Hashtable是线性安全的,HashMap是线性不安全的,所以后者效率更高。

他们两个和HashSet集合不能保证元素顺序一样,HashMap和Hashtable也不能保证键值对的顺序。
判断key值相等的标准:两个key通过equals方法比较返回true,两个key的hashCode值也相等。
判断value值相等的标准:只要两个对象通过equals方法比较返回true即可。
不能修改集合中的key,否则程序再也无法准确访问到Map中被修改过的key。

LinkedHashMap
和HashSet中的LinkedHashSet一样,HashMap也有一个LinkedHashMap子类,使用双向链表来维护键值对的次序,迭代顺序和插入顺序保持一致。

常用API

API描述
V put(K key, V value)添加数据
V get(Object key)获取数据
Set keySet()获取所有的key
Collection values()获取所有的value
Set<Map.Entry<K, V>> entrySet()获取所有的键值对
boolean containsKey(Object key)是否包含某个键值

测试

/**
  * * 测试put()方法
  */
 @Test
 public void test01() {
     Map<String, Object> map = new HashMap<String, Object>();
     map.put("1", "1");
     map.put("2", "2");
     map.put("1", "3");
     System.out.println(map);// {1=3, 2=2} 长度为2
 }

 /**
  * 测试get()方法
  */
 @Test
 public void test02() {
     Map<String, Object> map = new HashMap<String, Object>();
     map.put("1", "1");
     map.put("2", "2");
     map.put("1", "3");
     System.out.println(map.size());// 2
     System.out.println(map.get("2"));// 2
 }

 /**
  * 测试迭代Map获取map中所有的key
  */
 @Test
 public void test03() {
     Map<String, Object> map = new HashMap<String, Object>();
     map.put("1", "1");
     map.put("2", "2");
     map.put("1", "3");
     // 获取map中所有的key
     Set<String> keys = map.keySet();

     for (String key : keys) {
         System.out.println(key);// 1 2
         System.out.println(map.get(key));// 3 2
     }
 }

 /**
  * 获取所有的value
  */
 @Test
 public void test05() {
     Map<String, Object> map = new HashMap<String, Object>();
     map.put("1", "1");
     map.put("2", "2");
     map.put("1", "3");

     Collection<Object> values = map.values();
     for (Object v : values) {
         System.out.println("value:" + v);// value:3 value:2
     }
 }

 /**
  * 获取所有的键值对
  */
 @Test
 public void test06() {
     Map<String, Object> map = new HashMap<String, Object>();
     map.put("1", "1");
     map.put("2", "2");
     map.put("1", "3");

     Set<Entry<String, Object>> entrySet = map.entrySet();
     for (Entry<String, Object> e : entrySet) {
         String key = e.getKey();
         Object value = e.getValue();
         System.out.println(key + ":" + value);// 1:3 2:2
     }
 }

 /**
  * 测试containsKey()方法
  */
 @Test
 public void test07() {
     Map<String, Object> map = new HashMap<String, Object>();
     map.put("1", "1");
     map.put("2", "2");
     map.put("1", "3");
     System.out.println(map.containsKey("2"));// true
     System.out.println(map.containsKey("3"));// false
 }

 /**
  * 测试使用Hashtable
  */
 @Test
 public void test08() {
     Map<String, Object> map = new Hashtable<String, Object>();
     map.put("1", "1");
     map.put("2", "2");
     map.put("1", "3");
     // 获取map中所有的key
     Set<String> keys = map.keySet();
     for (String key : keys) {
         System.out.println(map.get(key));// 2 3
     }
 }

队列和栈

队列特殊的线性表,队列中限制了对线性表的访问只能从线性表的一端添加元素,从另一端取出,遵循先进先出(FIFO)原则。

栈是Queue的子接口,定义类"双端列"从队列的两端可以入队(offer)和出队(poll),LinkedList实现了该接口,如果限制Deque双端入队和出队,将双端队列改为单端队列即为栈,栈遵循先进后出(FILO)的原则。

测试

/**
  * 测试队列 特殊的线性表,队列中限制了对线性表的访问只能从线性表的一端添加元素,从另一端取出,遵循先进先出(FIFO)原则
  */
 @Test
 public void test01() {
     Queue<String> que = new LinkedList<String>();
     // 添加队尾
     que.offer("3");
     que.offer("1");
     que.offer("2");

     // 获取队首
     String str = que.peek();
     System.out.println(str);// 3

     // 移除队首
     que.poll();
     System.out.println(que);// [1, 2]
 }

 /**
  * 栈是队的子接口,栈是继承队的,定义类"双端列"从队列的两端可以入队(offer)和出队(poll)
  * LinkedList实现了该接口,如果限制Deque双端入队和出队,将双端队列改为单端队列即为栈,栈遵循先进后出(FILO)的原则
  */
 @Test
 public void test02() {
     Deque<String> stack = new LinkedList<String>();
     // 压栈
     stack.push("aaa");
     stack.push("bbb");
     stack.push("ccc");
     System.out.println(stack);// [ccc, bbb, aaa]

     // 弹栈
     String lastStr = stack.pop();
     System.out.println(lastStr);// ccc
     lastStr = stack.pop();
     System.out.println(lastStr);// bbb
     lastStr = stack.pop();
     System.out.println(lastStr);// aaa
 }

15、异常

由于一些特殊的现象导致我们的java程序意外终止,这种现象称为异常现象。

java中把异常现象的各种原因做了描述,每个描述称为异常类,java中提供了异常处理机制,可以避免程序的意外终止,这种机制称为异常处理。

异常处理提高java程序的健壮性,避免因为异常的出现,程序意外终止。

java中所有的异常类都在一个继承体系内

Throwable
		|-Exception
			|-运行时异常
				|-NullPointerException
				|-ArrayOutofBoundException
				|-ClassCastException
				|-NumberFormatException
			|-检查异常
				|-IOException
				|-SQLException
				|-FileNotFoundException
				|-ClassNotFoundException
				|-............
		|-Error

模拟异常的现象,处理异常

try :可能出现异常的java代码,必须与catch或finally语句块一起使用
catch :捕捉异常,如果出现异常,具体处理的java代码
finally :最后总要执行
throw :抛出异常对象
throws :方法定义的时候,定义方法的异常列表

  • try…catch…finally
  • try…catch…catch…finally
  • try…finally

运行异常和检查异常

  • 方法如果抛出检查异常,必须处理,否则出现编译错误。
  • 方法如果抛出运行异常,不需要处理,编译能通过。
/**
  * 求工资总和的方法,工资必须是正数,否则该方法会抛出异常 当方法中抛出检查异常,方法必须声明该异
  */
 public double getSum(double x, double y) throws Exception {

     if (x <= 0 || y <= 0) {
         throw new Exception();
     }
     double result = x + y;
     return result;
 }

 @Test
 public void test01() {
     try {
         double sum = getSum(-1, 20000); //抛出异常
         System.out.println(sum);
     } catch (Exception e) {
         e.printStackTrace();// 捕捉到异常
         System.out.println("错误的工资信息");// 错误的工资信息
     }
 }

结果
java.lang.Exception
	.......................
错误的工资信息

 /**
  * 求工资总和的方法,工资必须是正数,否则该方法会抛出异常 当方法中抛出运行时异常,方法可以不声明该异常
  * 
  * @param x
  * @param y
  * @return
  * @throws RuntimeException 运行异常
  */
 public double getSum2(double x, double y) {

     if (x <= 0 || y <= 0) {
         throw new RuntimeException();
     }
     double result = x + y;
     return result;
 }

 @Test
 public void test02() {
     try {
         double sum = getSum2(-1, 15000);
         System.out.println(sum);
     } catch (RuntimeException e) {
         System.out.println("错误的工资信息");
     }
 }
结果
错误的工资信息

继承或实现中异常(重点)

  • 子类的方法中有异常抛出,那么就必须要交给父类。接口同理。

  • 子类重写父类的方法,如果父类的方法没有抛出异常,子类重写的方法不能抛出检查异常,可以抛出运行异常或者不抛出。 接口同理。

  • 父类中的方法抛出异常,子类中重写的方法抛出的异常的范围必须小于或等于父类中抛出的异常,子类重写的方法也可以不抛出异常。接口同理。

16、数据库

数据库语法

SQL语句描述
create database db01创建数据库
drop database db01删除数据库
show databases显示数据库
use db01使用数据库
use db01使用数据库

表语法

create table 表名(
     字段名1 类型类型(长度) 约束,
     字段名2 类型类型(长度),
     字段名3 类型类型(长度)
);

表名字段名,只能由字母,数字,下划线,$组成不能由数字开头不能是SQL的关键字。

数据类型包括,int 整数 int(6),double 浮点 dounle(8,2), varchar 可变长字符类型 varchar(4),char 不可变长字符类型 varchar(18),date 日期类型。

SQL语句描述
drop table t_student1删除表
show tables查看数据库中的表
drop table if exists t_student1;create table t_student1…先删除再创建

主键约束(非空,唯一)

create table t_user(
	id int(10) primary key,
	username varchar(30),
	password varchar(30),
	email char(30),
	phone int(11),
	creattime timestamp,
	endupdatetime timestamp
);

auto_increment(自动增长,用来生成主键字段的值)

create table t_user(
	 id int(10) primary key auto_increment,
	 username varchar(30),
	 password varchar(30),
	 email char(30),
	 phone int(11),
	 creattime timestamp,
	 endupdatetime timestamp,
  );

外键约束(外键字段必须引用于主键字段,被引用的表称为主表,引用的表称为从表)

添加外键约束1

create table t_item_cart(
    id int(8) primary key auto_increment,
    userid int(6),
    num int (4),
    created timestamp,
    itemid int(5) references t_item(id)          
    );

添加外键约束2

create table t_item_cart(
    id int(8) primary key auto_increment,
    userid int(6),
    num int (4),
    created timestamp,
    itemid int(5),
	constraint t_item_cart_itemid_fk foreign key(itemid)references t_item(id)     
    );

通过修改表的方式添加外键

alter table t_item_cart add foreign key(itemid) references t_item(id)

唯一约束

create table t_user(
	id int(10) primary key auto_increment,
	username varchar(30),
	password varchar(30),
	email char(30) unique,
	phone int(11) unique,
	creattime timestamp,
	endupdatetime timestamp
);

非空约束

create table t_user(
	id int(10) primary key auto_increment,
	username varchar(30) not null,
	password varchar(30) not null,
	email char(30) unique,
	phone int(11) unique,
	creattime timestamp,
	endupdatetime timestamp
);

检查约束

create table t_user(
	id int(10) primary key auto_increment,
	username varchar(30) not null,
	password varchar(30) not null,
	email char(30) unique,
	phone int(11) unique,
	creattime timestamp,
	endupdatetime timestamp,
	check(created>'2019-1-1')
);

设置一个字段的默认值

create table t_student(
	id int(10) primary key auto_increment,
	name varchar(10),
	gendar char(1) default '0',
	age int(2),
	addr varchar(30),
	birth date,
	idcard char(18)
);

mysql中的时间

date
datetime
timestamp

设置系统当前时间:

birth date default now(),
starttime datetime default now(),
paytime timestamp default current_timestamp(),

MSQL解决乱码

create database mydb default charset=utf8;
	create table t_student(
		id int(10) primary key auto_increment,
		name varchar(10),
		gendar char(1) default '0',
		age int(2),
		addr varchar(30),
		birth date,
		idcard char(18)
)default charset=utf8;

DQL:数据查询语言 select查询向数据库发送select语句,数据库会返回结果集

基本查询

	1、列出公司中员工的姓名,薪水,职位,入职日期
	
		select ename,sal,job,hiredate from emp;
	
	2、列出公司中所有的员工信息
	
		select * from emp;

条件查询
	
	1、列出职位是经理员工的姓名、薪水、入职日期,经理工号
	
		select ename,sal,hiredate,manager from emp where job='软件工程师';
	
	2、列出2018年之前入职的经理信息
	
		select * from emp where job='经理'and hiredate<'2018-1-1';
	
	3、列出薪水低于20000或或职位不是经理的员工的姓名,职位,薪水,入职日期
	
		select ename,job,sal,hiredate from emp where sal<2000 or job<>'经理';
	
	4、等值的SQL语句
	
		select * from t_user where username='111'and password='123';
		
		select * from t_user where username='111';
	
	5、列出102030,部门的经理姓名,入职日期,薪水,奖金
	
		select ename,hiredate,sal,com from emp where job='经理'and (deptno=10 or deptno=20 or deptno=30);
		
		select ename,hiredate,sal,com from emp where job='经理'and deptno in(10,20,30);
	
模糊查询
	
	like
	%:任意位任意字符
	_:一位任意字符
	
	1、列出员工姓y的员工的姓名、薪水、职位、入职日期
	
		select ename,sal,job,hiredate from emp where ename like 'y%';
	
	2、列出员工名字中第二个字是'c'的员工的姓名、薪水、职位、入职日期
	
		select ename,sal,job,hiredate from emp where ename like '_c%';
	
	between...and...
	not between...and...
	
	1、列出工资在1500020000之间,16年之前,18年之后的员工的姓名、薪水、职位、入职日期
	
		select ename,sal,job,hiredate from emp where sal between 15000 and 20000 and hiredate  not between '2016-1-1'and '2018-12-31' ;

排序查询

	order by 参考字段
	asc 升序
	desc 降序
	
	1、列出员工信息,根据工资的降序排序
	
		select * from emp order by sal desc; 
	
	2、列出员工信息,根据工资降序排序,入职日期升序排序
	
		select * from emp order by sal desc,hiredate asc;

分组查询
	
	注意:只能查询分组字段,以及组函数的运算结果
	
	group by 字段名
	
	组函数:
		sum();	 sum(sal);
		avg();	 ang(sal);
		max();	 max(sal);
		min();	 min(sal);
		count(); 统计数量
	
	1、统计每一个部门员工薪水的总和
	
		select deptno,sum(sal) from emp group by deptno;
	
	2、统计每一个部门员工薪水的平均值
	
		select deptno,avg(sal) from emp group by deptno;
	
	3、统计每一个部门员工薪水的最大值
	
		select deptno,max(sal) from emp group by deptno;
	
	4、统计每一个部门员工薪水的最小值
	
		select deptno,min(sal) from emp group by deptno;
	
	5、统计每一个部门有多少个员工
	
		select deptno,count(*) from emp group by deptno;
		
		select deptno,count(empno) from emp group by deptno;
		
		select deptno,count(*) as yang from emp group by deptno;  结果起别名
		
		select deptno,count(*) yang from emp group by deptno;     结果起别名 as可以省略
	
	6、列数工资大于20000的员工的姓名,职位,薪水
	
		select e.ename,job as j,sal as '工资' from emp as e where sal>20000;  结果起别名  from->where->select
		
		select e.ename,job as j,sal as s from emp as e where s>20000;         报错 执行顺序问题
	
	7、列出公司中一共多少个员工
	
		select count(*) from emp;   用来查询一个表中的总记录数
	
	8、列出10,20,30员工的平均薪水
	
		select deptno,avg(sal) from emp where deptno in(10,20,30) group by deptno;
	
	9、将结果取整
	
		select deptno,avg(sal),round(avg(sal)),floor(avg(sal)) from emp where deptno in(20,30) group by deptno;
		
		round(avg(sal))  取整

	having

		对分组后的结果再过滤
	
	1、列出公司中10,20部门平均工资大于25000的部门号和平均工资
	
		select deptno,avg(sal) from emp where deptno in (10,20) group by deptno having avg(sal)>25000;
	
	分页查询
	
	内存分页
	数据据分页:limit i1,i2
	i1表示起点
	i2表示获取的记录数
	
	select * from emp limit 0,3;  
	select * from emp limit 3,3;   
	select * from emp limit 6,3;
	
	int perPage=3;                 每页现实的记录数
	int page=1;                    页数
	int begin=(page-1)*perPage;    计算起点
	
	查询一个表的总记录数tatalRecords
		select count(*) from emp;
	
	计算最大页数
		int maxPage=tatalRecords%perPage==0?tatalRecords/perPage:tatalRecords/perPage+1;
	
		上一页 首页 1 2 3 4 末页 下一页
	
		select * from emp order by hiredate desc limit 0,3;
	
	需求:列出部门的平均薪水,要根据部门的平均薪水降序排序,在平均薪水中,只显示平均薪水低于20000的部门号和平均薪水,以上数据中只统计102030号部门
	
		select deptno,avg(sal) from emp where deptno in (10,20,30) group by deptno having avg(sal)<20000 order by avg(sal) desc;

子查询

	普通子查询

		1、列出yang所在部门的同事信息

			select * from emp where deptno=(select deptno from emp where ename='yang');

			select * from emp where deptno=(select deptno from emp where ename='yang')and ename!='yang';

	相关子查询

		1、列出比本部门平均薪资低的员工姓名,薪水,职位,部门号

			select ename,sal,job,deptno from emp as e where sal<(select avg(sal) from emp where deptno=e.deptno);

		2、列出公司中领导的姓名,职位,薪水

		exists

			select ename,job,sal from emp e where exists(select * from emp where manager=e.empno);

		in

			select ename,job,sal from emp where empno in(select distinct manager from emp);

		3、列出公司中普通职员的姓名,职位,薪水

		not exists

			select ename,job,sal from emp e where not exists(select * from emp where manager=e.empno);

		not in

			select ename,job,sal from emp where empno not in(select distinct manager from emp where manager is not null);

联合查询

	1、列出员工姓名,薪水,职位,入职日期,部门号,所在的部门名和部门地址

		select ename,sal,job,hiredate,e.deptno,dname,loc from emp e,dapt d;    笛卡尔乘积现象         7*4=28

	笛卡尔乘积现象:多表查询没有指定结果集的连接条件,数据库管理系统会将两个表或多个表的数据分别链接一次

	在联合查询的时候,指定连接的条件,可以避免出现笛卡尔乘积现象

	条件数量最少n-1

		select ename,sal,job,hiredate,e.deptno,dname,loc from emp e,dapt d where e.deptno=d.deptno; 

	联合查询中,根据对数据的要求不同可以使用内连接和外连接查询

	内连接:只查询满足链接条件的数据

	inner join...on...

		select ename,sal,job,hiredate,e.deptno,dname,loc from emp e inner join dept d on e.deptno=d.deptno; 

	等值查询和内连接查询结果相同

	外连接:

	       左外连接:left outer join...on...   outer可写可不写

	       		select ename,sal,job,hiredate,e.deptno,dname,loc from emp e left outer join dept d on e.deptno=d.deptno; 

	       右外连接:right outer join...on...

	       		select ename,sal,job,hiredate,e.deptno,danme,loc from emp e right outer join dept d on e.deptno=d.deptno; 

	 1、将工资大于10000的员工信息与2010年之后入职的员工信息的数据的集合,两个集合的并集
	 
 		union(对多个结果集去重的连接操作)
 	
	 		select * from emp where sal>10000 union select * from emp where hiredete>'2010-1-1';

		 union all(对多个结果集不去重的连接操作)

	 		select * from emp where sal>10000 union all select * from emp where hiredete>'2010-1-1';

DML语句:数据操作语言 insert update delete

insert语句
insert into table values(.......);
insert into table (c1,c2,c3)values(v1,v2,v3);

复制表:复制结构和数据
create table emp_copy as select * from emp;

update语句,编辑表数据
update emp_copy set sal=sal+8000 where empno!=7000;
update emp_copy set sal=sal+8000 where empno=7001;

delete语句,删除表数据
delete from emp_copy;
delete from emp_copy where empno=7001;

DDL(数据定义语言):create、drop、alter

create table dept(
	deptno int (5) primary key auto_increment,
	danme varchar(10),
	loc varchar(30)
);
alter table emp add foreign key(deptno) references dept(deptno);

17、JDBC

mysql.properties 文件

jdbc.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/yang?useSSL=true
jdbc.user=root
jdbc.password=1399
jdbc.init=20
jdbc.max=100

JDBC工具类用于加载驱动,创建连接和关闭连接如下

/**
 * JDBC工具类
 * 
 * 1.加载驱动 2.创建连接 3.关闭连接
 *
 */
class JdbcUtil {

    private static String className;
    private static String url;
    private static String user;
    private static String password;

    private JdbcUtil() {}

    static {
        try {
        	// 初始化变量
            initParam();
            // 加载驱动
            Class.forName(className);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void initParam() throws IOException {
        Properties prop = new Properties();
        // 获取指向当前项目类路径中某个文件的输入流
        InputStream is = JdbcUtil.class.getClassLoader().getResourceAsStream("mysql.properties");
        prop.load(is);
        className = prop.getProperty("jdbc.className");
        url = prop.getProperty("jdbc.url");
        user = prop.getProperty("jdbc.user");
        password = prop.getProperty("jdbc.password");
        is.close();

    }

	// 创建连接方法
    public static Connection getConn() throws SQLException {
        /**
         * Connection,接口 表示连接对象接口 DriverManager类:驱动管理器 Connection getConnection(url,user,password)
         *
         * url: jdbc:mysql:// 决定驱动管理器调用哪个驱动 localhost:3306/ 决定数据库软件的具体地址 jdbcdb01 决定连接具体的数据库 ?useSSL=true
         * 高版本mysql会提示一个安全性警告
         *
         * user: 数据库的账号 password: 数据库的密码
         *
         */
        Connection conn = DriverManager.getConnection(url, user, password);
        return conn;
    }

	// 关闭连接方法
    public static void close(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

在此工具类的基础上做DML数据操作语言(增删改)和DQL数据查语言(查询)操作。并且有两种连接数据库的方式,一种是Statement,另一种是prepareStatement。

Statement

DML数据操作语言

@Test
public void test01() throws Exception {

    Connection conn = JdbcUtil.getConn();
    Statement stmt = conn.createStatement();
    // 增加操作
    String sql1 = "INSERT INTO user values(1,'y','12');";
    int i = stmt.executeUpdate(sql1);
    if (i > 0) {
        System.out.println("插入了一条数据");
    }
    // 删除操作
    String sql2 = "DELETE FROM user WHERE NAME = 'y';";
    int j = stmt.executeUpdate(sql2);
    if (i > 0) {
        System.out.println("删除了一条数据");
    }
    // 修改操作
    String sql3 = "UPDATE user SET NAME = 'c' WHERE NAME = 'y';";
    int k = stmt.executeUpdate(sql3);
    if (i > 0) {
        System.out.println("修改了一条数据");
    }
    JdbcUtil.close(conn);
}	

DQL数据查语言

@Test
public void test02() throws Exception {

    Connection conn = JdbcUtil.getConn();
    Statement stmt = conn.createStatement();

    String sql = "SELECT * FROM test;";
    // 处理结果集
    ResultSet rs = stmt.executeQuery(sql);
    // boolean next() 从前往后移动结果集指针 默认结果集指针指向第一条记录之前的位置 每调用一次,结果集指针默认从前往后移动一行 如果有数据就返回true 没有就返回false
    while (rs.next()) {
        int id = rs.getInt("id");
        String name = rs.getString("name");
        String code = rs.getString("code");

        System.out.print(id + "\t");
        System.out.print(name + "\t\t");
        System.out.print(code);
        System.out.println();
    }
    JdbcUtil.close(conn);
}

结果
1	a		T12
2	b		45
3	c		T1
4	d		56
5	e		23
6	bb		jj

prepareStatement

DML数据操作语言

@Test
public void test01() throws SQLException {

    Connection conn = JdbcUtil.getConn();

    // 增加操作
    String sql1 = "INSERT INTO test values(?,?,?);";
    PreparedStatement pstmt1 = conn.prepareStatement(sql1);
    pstmt1.setInt(1, 12);
    pstmt1.setString(2, "y");
    pstmt1.setString(3, "c");

    int i = pstmt1.executeUpdate();
    if (i > 0) {
        System.out.println("插入一条数据");
    }

    // 删除操作
    String sql2 = "DELETE FROM test WHERE ID = '1';";
    PreparedStatement pstmt2 = conn.prepareStatement(sql2);
    int j = pstmt2.executeUpdate(sql2);
    if (j > 0) {
        System.out.println("删除了一条数据");
    }
    // 修改操作
    String sql3 = "UPDATE test SET NAME = 'm' WHERE ID = '12';";
    PreparedStatement pstmt3 = conn.prepareStatement(sql3);
    int K = pstmt3.executeUpdate(sql3);
    if (K > 0) {
        System.out.println("修改了一条数据");
    }
    JdbcUtil.close(conn);
}

DQL数据查语言

@Test
public void test02() throws SQLException {

    Connection con = JdbcUtil.getConn();

    String sql = "SELECT * FROM test WHERE ID = ? AND NAME = ?";

    // 实例化
    PreparedStatement pstmt = con.prepareStatement(sql);

    // 装载占位符
    pstmt.setString(1, "6");
    pstmt.setString(2, "bb");

    // 执行sql语句
    ResultSet rs = pstmt.executeQuery();

    System.out.println(rs);
    while (rs.next()) {
        System.out.println(rs.getInt("id") + " " + rs.getString("name"));
    }

    JdbcUtil.close(con);
}

结果
com.mysql.jdbc.JDBC4ResultSet@4f4a7090
6 bb

拓展使用DBCP连接池创建工具类

/**
 * JDBC工具类
 * 
 * 1.加载驱动 2.创建连接 3.关闭连接
 *
 */
public class JdbcUtil2 {

    private static BasicDataSource ds = new BasicDataSource();

    private JdbcUtil2() {}

    static {
        try {
            initParam();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 初始化连接池参数
    public static void initParam() throws IOException {
        Properties prop = new Properties();
        // 获取指向当前项目类路径中某个文件的输入流
        InputStream is = JdbcUtil.class.getClassLoader().getResourceAsStream("mysql.properties");

        prop.load(is);

        String className = prop.getProperty("jdbc.className");
        String url = prop.getProperty("jdbc.url");
        String user = prop.getProperty("jdbc.user");
        String password = prop.getProperty("jdbc.password");
        String strInit = prop.getProperty("jdbc.init");
        String strMax = prop.getProperty("jdbc.max");

        ds.setDriverClassName(className);
        ds.setUrl(url);
        ds.setUsername(user);
        ds.setPassword(password);
        ds.setInitialSize(Integer.parseInt(strInit));
        ds.setMaxActive(Integer.parseInt(strMax));

        is.close();

    }

    public static Connection getConn() throws SQLException {
        Connection conn = ds.getConnection();
        return conn;
    }

    public static void close(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

18、线程

什么是进程

是包含了某些资源的内存区域,操作系统利用进程把它的工作划分为一些功能单元。电脑中时会有很多单独运行的程序,每个程序有一个独立的进程。例如微信,IDEA,GOOGLE等等。

在这里插入图片描述

什么是线程

进程中包含的一个或多个执行单元称为线程,线程只能归属一个进程,并且线程只能访问该进程拥有的资源。当操作系统创建一个进程,该进程会自动申请一个主线程作为首要的执行任务。线程的切换耗时小,把线程称为轻负荷线程。一个进程由一个或多个线程组成,彼此间完成不同的工作,多个线程同时执行,称为多线程。

进程和线程的关系

  • 一个进程由一个或多个线程组成。
  • 线程的划分尺度小于进程。
  • 多个进程在执行过程中拥有独立的内存单元,而多个线程共享内存。

什么是并发和并行

并发

并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

翻译成人话就是:

我们在打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。(就像前面提到的操作系统的时间片分时调度)

并行

并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

如何使用线程

(1)通过继承Thread类,重写run方法,线程的任务定义在run()方法中。

class TicketThread extends Thread {
    private int tickets = 30;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
            } else {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                break;
            }
        }
    }
}

创建线程对象和启动线程

// 创建一个线程对象
TicketThread t = new TicketThread();
// 启动线程
t.start();

(2)实现Runnable接口,实现run方法,线程的任务,定义在run()方法中。

class TicketThread implements Runnable {
    private int tickets = 30;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
            } else {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                break;
            }
        }
    }
}

创建线程对象和启动线程

// 创建一个任务对象
Runnable runnable = new TicketThread();
// 创建一个线程对象
Thread t = new Thread(runnable);
// 启动线程
t.start();

(3)实现Callable接口。

class TicketThread implements Callable<List<String>> {
    private int tickets = 30;
    List<String> list = new ArrayList<String>();

    @Override
    public List<String> call() throws Exception {

        while (true) {
            if (tickets > 0) {
                list.add(Thread.currentThread().getName() + "正在买票" + tickets--);
            } else {
                list.add(Thread.currentThread().getName() + "票卖完了");
                return list;
            }
        }
    }
}

创建线程对象和启动线程

Callable callable = new TicketThread();
FutureTask futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// 获取返回值
List<String> list = (List<String>)futureTask.get();
for (String s : list) {
    System.out.println(s);
}

Thread和Runnable两种开发线程的区别

  1. 继承Thread类,不能实现多个线程共享同一个实例资源,实现Runnable接口,任务模块化,多个线程可以共享同一个资源。
  2. 继承Thread类,不能继承其他类,有单继承局限性,实现Runnable接,还可以继承其他的父类。

结论:开发中通常使用实现Runnable接口,开发多线程应用。

Thread 类中的API

方法描述
static Thread currentThread()获取当前线程对象(线程名称, 线程优先级, 线程所属线程组)
String getName()获取当前线程对象
viod set(String name)设置线程名称
int getId()获取线程id
int getPriority(int i)设置线程级别
static Thread currentThread()获取当前线程对象
void setDaemon(boolean bo)设置一个线程为守护(后台)线程
boolean isDaemon()获取守护线程是true还是false
static native void sleep(long millis)设置休眠(单位毫秒)
static native void yield()当前线程放弃时间片
void join()等待该线程终止
void wait()设置当前线程等待阻塞状态
void notify()唤醒正处于等待状态的线程
void notifyAll()唤醒所有处于等待状态的线程

详细介绍下线程中的 join() 方法

首先有个需求,模拟图片的下载和查看 图片下载完毕,才可以查看图片,创建两个线程,一个是图片下载的线程

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i <= 5; i++) {
            System.out.println("t1:正在下载:" + i * 20 + "%");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("图片下载完成");
    }
});

一个是图片查看的线程

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("t2:查看图片");
    }
});

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

当我们一起启动这两个线程的时候,会发现不能使线程t2在线程t1后执行,执行效果为

t1:正在下载:0%
t2:查看图片
t1:正在下载:20%
t1:正在下载:40%
t1:正在下载:60%
t1:正在下载:80%
t1:正在下载:100%
图片下载完成

很明显这个十分不合理,想要解决这个问题就要使用join方法,其方法作用就是等待线程终止,在线程t2中如果调用线程t1的join方法,表示只有当线程t1执行完毕时,线程t2才能继续执行。线程t2修改为

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t2:查看图片");
    }
});

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

结果为

t1:正在下载:0%
t1:正在下载:20%
t1:正在下载:40%
t1:正在下载:60%
t1:正在下载:80%
t1:正在下载:100%
图片下载完成
t2:查看图片

可以看到现在的效果是完成了我们的需求,还可以在主线程中调用t1的join方法,我们知道主线程是优于子线程t1和t2执行的,如果这样调用则会让t2在主线程之前进行,这样也满足了t1在t2之前执行,代码为

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("t2:查看图片");
    }
});

t1.start();
t1.join();
t2.start();

我们还需要注意的是

  • join方法必须在线程start方法调用之后调用才有意义。
  • join方法中如果传入参数,则表示这样的意思:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。
  • join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。(其实join()中调用的是join(0))

线程的状态(生命周期)

  1. 新建状态(new):创建一个线程对象。
  2. 就绪状态(Runnable):线程对象创建之后,调用start()方法,就绪状态的线程只处于等待CPU的使用权,变为可运行。
  3. 运行状态(Running): 就绪状态的线程,获取到了CPU资源,执行程序代码。
  4. 阻塞状态(Blocked): 等待阻塞(线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其他线程执行了该对象的notify()或notifyAll()方法,才可能将其唤醒)、线程阻塞(线程获取synchronized同步锁失败(因为锁被其它线程锁占用),它会进入同步阻塞状态)、其它阻塞(通过调用线程的sleep()或join()或发出了IO请求时,线程就会进入阻塞状态。当sleep()超时、join()等待线程终止或超时、或者IO处理完毕时,线程重新转入就绪状态)。
  5. 死亡状态(Dead):线程任务执行结束,即run()方法结束,该线程对象就会被垃圾回收,线程对象即为死亡状态。

线程安全问题

多个线程必须访问同一个资源,对同一个变量运算,就产生了线程安全问题。

举例:

public class Ticket {
    public static void main(String[] args) {
        Runnable r = new runnable();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
    }
}

class runnable implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                break;
            }
        }
    }
}

可以看到两个线程买到了同一张票,这种就是线程安全问题。

Thread-0正在买票100
Thread-1正在买票99
Thread-0正在买票98
Thread-1正在买票98
Thread-0正在买票97
Thread-1正在买票97
Thread-1正在买票96
Thread-0正在买票96
Thread-0正在买票94
..................

解决线程安全问题的方式有三种

(1)synchronized 修饰同步方法块,指定加锁对象(可以是实例对象,也可以是类变量),对给定对象加锁,进入同步方法块时需要获得加锁对象的锁。

class runnable implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {// this为实例对象,也可以使用类变量.calss
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println(Thread.currentThread().getName() + "票卖完了");
                    break;
                }
            }
        }
    }
}

问题解决

Thread-0正在买票100
Thread-1正在买票99
Thread-1正在买票98
Thread-1正在买票97
Thread-1正在买票96
Thread-1正在买票95
Thread-1正在买票94
Thread-0正在买票93
Thread-0正在买票92
..................

(2)synchronized 修饰实例方法,给当前实例变量加锁,进入同步方法时需要获得当前实例的锁。
         synchronized 修饰静态方法,给当前类对象加锁,进入同步方法时需要获得类对象的锁。

class runnable implements Runnable {
    private static int tickets = 100;

    @Override
    public void run() {
        while (true) {
            try {
                buy();
                //buyStatic();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (tickets <= 0) {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                break;
            }
        }
    }

    // 修饰实例方法
    public synchronized void buy() throws InterruptedException {
        if (tickets > 0) {
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
        }
    }

    // 修饰静态方法
    public synchronized static void buyStatic() throws InterruptedException {
        if (tickets > 0) {
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
        }
    }
}

问题解决

Thread-0正在买票100
Thread-1正在买票99
Thread-1正在买票98
Thread-1正在买票97
Thread-1正在买票96
Thread-1正在买票95
Thread-1正在买票94
Thread-0正在买票93
Thread-0正在买票92
..................

(3)使用锁对象 ReentrantLock。

class runnable implements Runnable {
    private int tickets = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
                } else {
                    System.out.println(Thread.currentThread().getName() + "票卖完了");
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

问题解决

Thread-0正在买票100
Thread-1正在买票99
Thread-1正在买票98
Thread-1正在买票97
Thread-1正在买票96
Thread-1正在买票95
Thread-1正在买票94
Thread-0正在买票93
Thread-0正在买票92
..................

线程安全的单例模式

单例模式分为饿汉模式和懒汉模式,单例模式的效果就是保证某个类只有唯一实例。

(1)饿汉单例模式

public class Hungry_Person {

    private static Hungry_Person single = new Hungry_Person();

    private Hungry_Person() {}

    public static Hungry_Person getInstance() {

        return single;
    }
}

测试结果,两个对象相等

Hungry_Person obj1 = Hungry_Person.getInstance();
Hungry_Person obj2 = Hungry_Person.getInstance();
System.out.println(obj1 == obj2);

结果
true

(2)懒汉单例模式

public class Lazy_Person {
    private static Lazy_Person single;

    private Lazy_Person() {
        System.out.println("对象被创建了!");
    }

    public static Lazy_Person getInstance() {
        if (single == null) {
            single = new Lazy_Person();
        }
        return single;
    }
}

测试结果,两个对象相等

Lazy_Person obj1 = Lazy_Person.getInstance();
Lazy_Person obj2 = Lazy_Person.getInstance();
System.out.println(obj1 == obj2);

结果
对象被创建了!
true

但是懒汉单例模式可能出现的线程安全问题

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        Lazy_Person.getInstance();
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        Lazy_Person.getInstance();
    }
});
t1.start();
t2.start();

结果
对象被创建了!
对象被创建了!

在多线程种会出现创建两个不同对象的情况,这就是线程安全问题,可以给懒汉模式在创建对象的时候加 synchronized


public class Lazy_Person {
    private static Lazy_Person single;

    private Lazy_Person() {
        System.out.println("对象被创建了!");
    }

    public static Lazy_Person getInstance() {
        if (single == null) {
            synchronized (Lazy_Person.class) {
                if (single == null) {
                    single = new Lazy_Person();
                }
            }
        }
        return single;
    }
}


结果
对象被创建了!

第一个判定条件是否需要加锁,第二个判定条件是否需要对像实例化,这就解决了懒汉模式下的线程安全问题。

线程的死锁

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

模拟死锁代码

public class Die_synchronized {
    public static String resource1 = "resource1";
    public static String resource2 = "resource2";

    public static void main(String[] args) {
        Thread thread1 = new Thread(new BusinessA());
        Thread thread2 = new Thread(new BusinessB());
        thread1.start();
        thread2.start();
    }

    static class BusinessA implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("BusinessA启动");
                while (true) {
                    synchronized (Die_synchronized.resource1) {
                        System.out.println("BusinessA拿到了resource1的锁");
                        Thread.sleep(3000);// 获取resource1后先等一会儿,让BusinessB有足够的时间锁住resource2
                        System.out.println("BusinessA想拿resource2的锁。。。。");
                        synchronized (Die_synchronized.resource2) {
                            System.out.println("BusinessA获得到了resource2的锁");
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    static class BusinessB implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("BusinessB启动");
                while (true) {
                    synchronized (Die_synchronized.resource2) {
                        System.out.println("BusinessB拿得到了resource2的锁");
                        Thread.sleep(3000);// 获取resource2后先等一会儿,让BusinessA有足够的时间锁住resource1
                        System.out.println("BusinessB想拿resource1的锁。。。。");
                        synchronized (Die_synchronized.resource1) {
                            System.out.println("BusinessB获得到了resource1的锁");
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

结果死锁,就没有后续了

BusinessA启动
BusinessA拿到了resource1的锁
BusinessB启动
BusinessB拿得到了resource2的锁
BusinessA想拿resource2的锁。。。。
BusinessB想拿resource1的锁。。。。

Synchronized 同步方法的八种使用场景

来通过代码实现,分别判断以下场景是不是线程安全的。

(1)两个线程同时访问同一个对象的同步方法

public class SynchronizedYang {

    public synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        SynchronizedYang yang = new SynchronizedYang();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun1();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-0]  fun1-1
[Thread-0]  fun1-2
[Thread-1]  fun1-0
[Thread-1]  fun1-1
[Thread-1]  fun1-2

两个线程争夺同一个对象锁,所以两个线程串行执行的,是线程安全的。

(2)两个线程同时访问两个对象的同步方法

public class SynchronizedYang {

    public synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        SynchronizedYang yang1 = new SynchronizedYang();
        SynchronizedYang yang2 = new SynchronizedYang();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang1.fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang2.fun1();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-1]  fun1-0
[Thread-0]  fun1-1
[Thread-1]  fun1-1
[Thread-1]  fun1-2
[Thread-0]  fun1-2

两个线程有两个对象锁,不再争夺对象锁,所以两个线程是并行执行的,是线程不安全。这个问题是可以解决的,那就是加类锁,加类锁有两种,一个是在静态方法上加锁,一个是在代码块上加类锁,如下

public class SynchronizedYang {

    public void fun1() {
        synchronized (SynchronizedYang.class) {
            try {
                for (int i = 0; i < 3; i++) {
                    System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                    Thread.sleep(1000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        SynchronizedYang yang1 = new SynchronizedYang();
        SynchronizedYang yang2 = new SynchronizedYang();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang1.fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang2.fun1();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-0]  fun1-1
[Thread-0]  fun1-2
[Thread-1]  fun1-0
[Thread-1]  fun1-1
[Thread-1]  fun1-2

(3)两个线程同时访问静态同步方法

public class SynchronizedYang {

    public static synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                fun1();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-0]  fun1-1
[Thread-0]  fun1-2
[Thread-1]  fun1-0
[Thread-1]  fun1-1
[Thread-1]  fun1-2

在静态方法上加锁就是类锁,所以串行,是线程安全的。

(4)两个线程分别同时访同步方法和非同步方法

public class SynchronizedYang {

    public static synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void fun2() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun2-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                fun2();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-1]  fun2-0
[Thread-1]  fun2-1
[Thread-0]  fun1-1
[Thread-1]  fun2-2
[Thread-0]  fun1-2

必然是并行,线程不安全的,如果不加方法synchronized能安全,那也就不需要同步方法了。

(5)两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法

public class SynchronizedYang {

    public synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
                fun2();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void fun2() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun2-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        SynchronizedYang yang = new SynchronizedYang();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun1();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-0]  fun2-0
[Thread-0]  fun2-1
[Thread-0]  fun2-2
[Thread-0]  fun1-1
[Thread-0]  fun2-0
[Thread-0]  fun2-1
[Thread-0]  fun2-2
[Thread-0]  fun1-2
[Thread-0]  fun2-0
[Thread-0]  fun2-1
[Thread-0]  fun2-2
[Thread-1]  fun1-0
[Thread-1]  fun2-0
[Thread-1]  fun2-1
[Thread-1]  fun2-2
[Thread-1]  fun1-1
[Thread-1]  fun2-0
[Thread-1]  fun2-1
[Thread-1]  fun2-2
[Thread-1]  fun1-2
[Thread-1]  fun2-0
[Thread-1]  fun2-1
[Thread-1]  fun2-2

串行执行,是线程安全的。

(6)两个线程同时访问同一个对象的不同的同步方法

public class SynchronizedYang {

    public synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void fun2() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun2-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        SynchronizedYang yang = new SynchronizedYang();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun2();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-0]  fun1-1
[Thread-0]  fun1-2
[Thread-1]  fun2-0
[Thread-1]  fun2-1
[Thread-1]  fun2-2

两个方法的synchronized修饰符,同一个对象调用,虽没有指定锁对象,但默认锁对象为锁对象,所以对于同一个对象实例,两个线程拿到的锁是同一把锁,这也是synchronized关键字的可重入性的一种体现。是串行执行,是线程安全的。

(7)两个线程分别同时访问静态synchronized和非静态synchronized方法

public class SynchronizedYang {

    public static synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void fun2() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun2-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        SynchronizedYang yang = new SynchronizedYang();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun2();
            }
        });
        t2.start();
    }
}

结果

[Thread-0]  fun1-0
[Thread-1]  fun2-0
[Thread-1]  fun2-1
[Thread-0]  fun1-1
[Thread-0]  fun1-2
[Thread-1]  fun2-2

这种场景的本质也是在探讨两个线程获取的是不是同一把锁的问题。静态synchronized方法属于类锁,锁对象是(*.class)对象,非静态synchronized方法属于对象锁中的方法锁,锁对象是this对象。两个线程拿到的是不同的锁,自然不会相互影响。是并行,是线程不安全的。

(8)同步方法抛出异常后,JVM会自动释放锁的情况

public class SynchronizedYang {

    public static synchronized void fun1() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun1-" + i);
                Thread.sleep(1000);
                throw new RuntimeException();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void fun2() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + " fun2-" + i);
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        SynchronizedYang yang = new SynchronizedYang();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                fun1();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                yang.fun2();
            }
        });
        t2.start();
    }
}

结果
[Thread-0]  fun1-0
[Thread-1]  fun2-0
[Thread-1]  fun2-1
java.lang.RuntimeException
	at SynchronizedYang.fun1(SynchronizedYang.java:8)
	at SynchronizedYang$1.run(SynchronizedYang.java:32)
	at java.base/java.lang.Thread.run(Thread.java:844)
[Thread-1]  fun2-2

在一个线程的同步方法中出现异常的时候,会释放锁,另一个线程得到锁,继续执行。而不会出现一个线程抛出异常后,另一个线程一直等待获取锁的情况。这是因为JVM在同步方法抛出异常的时候,会自动释放锁对象。

线程池

就是把若干用户线程添加到线程池中,由线程池统一管理线程。

为什么要使用线程池

  1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复使用或利用,可以并发执行多个任务。
  2. 可以根据系统的承受能力,调整线程池中的工作数目,防止消耗过多的内存而使服务器宕机。

线程池的使用

有一个Executors的线程工具类,此类提供了若干静态方法,这些静态方法用于生成线程池的对象。

(1)单线程的线程池创建

 ExecutorService pool = Executors.newSingleThreadExecutor();
        // 创建线程并放在线程池中
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();
        MyThread mt4 = new MyThread();
        MyThread mt5 = new MyThread();
        pool.execute(mt1);
        pool.execute(mt2);
        pool.execute(mt3);
        pool.execute(mt4);
        pool.execute(mt5);
        // 关闭线程池
        pool.shutdown();

结果
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...

创建一个单线程的线程池,这个线程池只有一个线程在工作,即单线程执行任务,如果这个唯一的线程因为异常结束,那么就会有一个新的线程来替代它因此线程池保证所有的任务是按照任务的提交顺序来执行。

(2)固定大小的线程池创建

ExecutorService pool=Executors.newFixedThreadPool(2);
		//创建线程并放在线程池中
		MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();
        MyThread mt4 = new MyThread();
        MyThread mt5 = new MyThread();
        pool.execute(mt1);
        pool.execute(mt2);
        pool.execute(mt3);
        pool.execute(mt4);
        pool.execute(mt5);
        // 关闭线程池
        pool.shutdown();
		
结果
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...

创建一个固定大小的线程池,每次提交一个任务就创建一个线程直到达到线程池的最大的大小,线程池的大小一旦达到最大就会保持不变,如果某个线程因为执行异常而结束,那么就会补充一个新的线程。

(3)可以缓冲的线程池创建

ExecutorService pool = Executors.newCachedThreadPool();
        // 创建线程并放在线程池中
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();
        MyThread mt4 = new MyThread();
        MyThread mt5 = new MyThread();
        pool.execute(mt1);
        pool.execute(mt2);
        pool.execute(mt3);
        pool.execute(mt4);
        pool.execute(mt5);
        // 关闭线程池
        pool.shutdown();
结果
pool-1-thread-1正在执行...
pool-1-thread-4正在执行...
pool-1-thread-2正在执行...
pool-1-thread-3正在执行...
pool-1-thread-5正在执行...

创建一个可以缓冲的线程池,如果线程大小超过处理任务所需的线程,那么就会回收部分线程,当线程数增加的时候此线程池不会对线程池大小做限制,线程池的大小完全依赖操作系统能够创建的做大做小。

(4)周期性线程池创建

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        pool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println("计划执行run");
            }
        }, 2000, 5000, TimeUnit.MILLISECONDS);

此线程池支持定时以及周期性的执行任务的需求。

手动创建线程池

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 30L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<Runnable>(5), new RejectedExecutionHandler() {
        // 回调方法
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.out.println("线程数超过了线程池容量,拒绝执行任务-->" + r);

        }

    });
结果
计划执行run
计划执行run
计划执行run
计划执行run
计划执行run
...........

在这里插入图片描述

19、文件

https://blog.csdn.net/yy139926/article/details/125030498

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值