JavaSE基础

发布时间 2023-10-24 11:39:19作者: 若乔

基础

输入

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner  scanner = new Scanner(System.in);
        String      name = scanner.next();
        int          age = scanner.nextInt();
        double    weight = scanner.nextDouble();
        boolean isSingle = scanner.nextBoolean();
        char      gender = scanner.next().charAt(0);
        scanner.close();
    }
}

二维数组静态初始化

int[][] arr1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9, 10}};
int[][] arr2 = new int[][]{{1, 2, 3}, {4, 5, 6}, {7, 8, 9, 10}};
int[][] arr3 = new int[3][3]{{1, 2, 3}, {4, 5, 6}, {7, 8, 9, 10}}; //错误,静态初始化右边new 数据类型[][]中不能写数字

length

length——数组的属性;

length()——String的方法;

size()——集合的方法;

String[] list={"a","b","c"};
System.out.println(list.length); //数组用length
String a="apple";
System.out.println(a.length()); //String用length()
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
System.out.println(list.size());//集合用size()

面向对象

super

class Person {
    protected String name; // protected之后,可以被子类访问
    protected int age;

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

class Student extends Person { //继承Person类
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age); // 必须super来调用父类的构造函数
        this.score = score;
    }
}

static

修饰成员变量、成员方法

public class Student {
    static String name; // 类变量,可以通过Student.name来访问,也可以通过对象.name来访问,所有对象都共用同一个name
    int age; // 实例变量,必须通过对象.name来访问
    public static void f() {} // 类方法,与类变量同理
    public void f2() {} // 实例方法,与实例变量同理
    private Student() {} // 构造器前面加上private就不能在其他地方构造这个对象了
}

注意事项

public class Student {
    static String schoolName; // 类变量
    double score; // 实例变量
    // 1、类方法中可以直接访问类成员,不可以直接访问实例成员
    public static void printHelloWorld() { // 类方法
        // 同一个类中,访问类成员,可以省略类名不写
        schoolName = "黑马";
        printHelloWorld2();
        // System.out.println(score); // 报错
        // printPass(); // 报错
        // this.printPass2(); // 报错
    }
    public static void printHelloWorld2() {} // 类方法
    // 2、实例方法中既可以直接访问类成员,也可以直接访问实例成员
    // 3、实例方法中可以出现this关键字,类方法中不可以出现this关键字
    public void printPass() { //实例方法
        schoolName = "黑马2";
        printHelloWorld2();
        System.out.println(score);
        printPass2();
        this.printPass2();
    }
    public void printPass2() {} // 实例方法
}

代码块

public class Student {
    static int number = 80;
    static String schoolName;
    /*
    静态代码块:
    格式:static{}
    特点:类加载时自动执行,类只会加载一次,所以静态代码块也执行一次
    作用:完成类的初始化,例如:对类变量的初始化赋值
    */
    static {
        System.out.println("静态代码块执行了~~");
        schoolName = "黑马";
    }
    /*
    实例代码块
    格式:{}
    特点:每次创建对象时,执行实例代码块,并在构造器前执行
    作用:和构造器一样,都是用来完成对象的初始化,例如:对实例变量进行初始化赋值(常用),可以用于记录日志,有人创建对象,就用实例代码块来记录,减少构造器重复代码
     */
    {
        System.out.println("实例代码块执行了~~");
        System.out.println("有人创建了对象:" + this);

    }
    public Student() {}
}

单例设计模式

饿汉式单例

public class A {
    // 2、定义一个类变量记住类的一个对象
    private static A a = new A();
    // 1、把类的构造器私有
    private A() {}
    // 3、定义一个类方法返回类的对象
    public static A getObject() { // 在外部无法通过构造器来创建对象,只能通过这个类方法来返回一个对象,而且无论调用多少次,只能创建这么一个对象,因为返回的a是类变量,属于类
        return a;
    }
}

懒汉式单例

public class A {
    // 2、定义一个类变量用于存储这个类的一个对象
    private static A a;
    // 1、把类的构造器私有
    private A() {}
    // 3、定义一个类方法,保证第一次调用时才创建一个对象,后面调用时都会用这同一个对象返回
    public static A getObject() {
        if (a == null) {
            a = new A();
        }
        return a;
    }
}

extends 继承

public class B extends A{}

类B用extends来继承A

权限修饰符

在这里插入图片描述

单继承

Java是单继承的(只能有一个爸爸),不支持多继承(一个孩子不能有多个爸爸),支持多层继承(爷爷、爸爸、孩子)

Object类

class A {}
class A extends Object {}

这俩等同,定义一个类A,默认继承自Object类

方法重写

子类重写一个参数列表一样的方法,去覆盖父类的这个方法,就是方法重写。
重写后,方法的访问,Java会遵循就近原则。

  • 小技巧:用Override注解,可以让编译器检查重写的格式是否正确,代码可读性更好
  • 子类重写父类方法时,访问权限必须大于或等于父类该方法的权限
  • 重写的方法返回值类型,必须与被重写方法的返回值类型一样,或者范围更小
  • 私有方法、静态方法不能被重写,会报错

子类访问其他成员

依照就近原则

public class A {
    String name = "父类名字";
}
public class B extends A{
    String name = "子类名字";
    public void showName() {
        String name = "局部名字";
        System.out.println(name); // 局部名字
        System.out.println(this.name); // 子类成员变量
        System.out.println(super.name); // 父类成员变量
    }
}

多态

什么是多态?
多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态。
多态的前提:有继承/实现关系;存在父类引用子类对象;存在方法重写。
多态的一个注意事项:多态是对象、行为的多态,Java中的属性(成员变量)不谈多态。
多态的具体代码体现

class People{
    void run(){
        System.out.println("People run");
    }
}
class Teacher extends People{
    @Override
    void run(){
        System.out.println("Teacher run");
    }
}
class Student extends People{
    @Override
    void run(){
        System.out.println("Student run");
    }
}
public class Test {
    public static void main(String[] args) {
        People p1 = new Student(); // 对象多态
        p1.run(); // Student run 行为多态
        People p2 = new Teacher();
        p2.run(); // Teacher run
    }
}

多态的好处:定义方法时,使用父类类型的形参,可以接受一切子类对象,扩展性更强,更便利
多态的缺点:无法使用子类独有的功能。

多态类型转换:
自动类型转换:父类 变量名 = new 子类();
强制类型转换:子类 变量名 = (子类)父类变量; 强制类型转换后可以解决多态无法使用子类独有功能的缺点。

final 终止

final关键字是终止的意思,可以修饰(类,方法,变量)
修饰类:该类被称为最终类,特点是不能被继承了
修饰方法:该方法成为最终方法,特点是不能被重写
修饰变量:该变量只能被赋值一次
注意:final修饰基本类型的变量,变量存储的数据不能被改变。final修饰引用类型的变量,变量存储的地址不能被改变,但地址所指向对象的内容是可以被改变的。

static final String SCHOOL_NAME= "传智教育"
static final修饰的成员变量是常量

abstract 抽象

抽象类,顾名思义,没办法实例化,无法创建对象
abstract,修饰类和方法

public abstract class A {
	public abstract void run(); // 抽象方法只有方法签名,一定不能有方法体
}

抽象类不一定有抽象方法,有抽象方法的类一定是抽象类。
抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
一个类继承抽象类,必须重写抽象类的全部抽象方法,否则这个类也必须定义成抽象类。

模板方法设计模式:解决代码重复的问题
在这里插入图片描述
模板方法设计模式应该怎么写?

  • 定义一个抽象类。
  • 在里面定义2个方法,一个是模板方法:放相同的代码里,一个是抽象方法:具体实现交给子类完成

建议使用final关键字修饰模板方法,防止被子类重写

abstract class People{
    // 1、定义一个模板方法
    final void write() { // 加个final关键字防止它被子类重写
        System.out.println("开头");
        // 2、模板方法并不清楚正文部分到底应该怎么写,但是它知道子类肯定要写
        System.out.println(writeMain());
        System.out.println("结尾");
    }
    // 3、设计一个抽象方法写正文,具体的实现交给子类来完成
    abstract String writeMain();
}
class Student extends People{
    @Override
    String writeMain(){
        return "学生正文";
    }
}
class Teacher extends People{
    @Override
    String writeMain(){
        return "老师正文";
    }
}
public class Main {
    public static void main(String[] args) {
        People p1 = new Student();
        People p2 = new Teacher();
        p1.write();
        p2.write();
    }
}

interface 接口

interface关键字可以定义一个特殊的结构:接口

public interface 接口名 {
	// 成员变量(接口中默认为常量)
	// 成员方法(接口中默认为抽象方法)
}

注意:接口不能创建对象;接口是用来被类实现(implements)的,实现接口的类成为实现类

public class 实现类 implements 接口1, 接口2, 接口3,...{
}

一个类可以实现多个接口(接口可以理解为干爹),实现类实现多个接口,必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类

interface B {
    void testb1();
    void testb2();
}
interface C {
    void testc1();
    void testc2();
}
class D implements B, C{
    @Override
    public void testb1(){}
    @Override
    public void testb2(){}
    @Override
    public void testc1(){}
    @Override
    public void testc2(){}
}

接口的好处:
弥补了类单继承的不足,一个类可以同时实现多个接口

class A extends Student implements Driver, Singer{}

A类的亲爹是Student,干爹是Driver和Singer

JDK8新增接口形式

public interface A{
	default void test1(){}
	private void test2(){}
	static void test3(){}
}

1、默认方法(实例方法):使用default修饰,默认会被加上public修饰,只能使用接口的实现类对象调用
2、私有方法:必须用private修饰
3、类方法(静态方法):使用static修饰,默认会被加上public修饰,只能用接口名来调用

匿名内部类

一种特殊的内部类,不需要为这个类声明名字

new 类或接口(参数值...) {
	类体(一般是方法重写);
};
new Animal() {
	@Override
	public void cry(){
	}
};

匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
匿名内部类通常作为一个参数传输给方法。
例如

public static void go(Swimming s){ // go函数传入一个Swimming类
	s.swim(); // 调用swim方法
}
go(new Swimming(){ // 匿名类Swimming作为参数传输给方法
	@Override;
	public void swim(){
		System.out.Println("狗游泳飞快");
	}
});

枚举类

修饰符 enum 枚举类名{
	名称1, 名称2;
	其他成员
}
public enum A {
	X, Y, Z; // 相当于
	public void go(){
	}
}
编译后得到以下
public final class A extends java.lang.Enum<A> {
	public static final A X = new A();
	public static final A Y = new A();
	public static final A Z = new A();
	public static A[] values();
	public static A valueOf(java.lang.String);
}
可以得出枚举类的特点
枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象
枚举类的构造器都是私有的,因此枚举类对外不能创建对象
枚举类都是最终类,不可以被继承
枚举类中,从第二行开始,可以定义类的其他各种成员

抽象枚举

public enum A {
    X(){
        @Override
        public void go(){...}//因为go是抽象方法,所以要重写
    }, Y("张三"){
        @Override
        public void go(){...}
    }; // 枚举了两个实例
    private String name;
    A(){} // 构造器默认私有的
    A(String name){ // 有参构造器
        this.name = name;
    }
    public abstract void go(); // 抽象方法,此时A是抽象类,不能直接构建对象,所以第一行不能罗列
}

泛型

泛型类

public class Array<E>{
	...
}
Array<String> arr = new Array<>(); // 使用方法

public class 类名<E, T, ...>{
	...
}
Array<String, Animal, ...> arr = new Array<>();  // 使用方法

public class 类名<E extends Animal>{
	...
}
Array<Cat> arr = new Array<>(); // 使用方法

泛型接口

public interface A<E>{...}

泛型方法

public static <T> void test(T t){...}

举个例子

import java.util.ArrayList;

public class Main{
    public static void main(String[] args){
        ArrayList<Car> cars = new ArrayList<>();
        go(cars);
    }
    // ? 通配符,在使用泛型的时候可以代表一切类型,E T K V是在定义的时候使用
    // ? extends Car 可以表示Car以及Car的子类
    // ? super Car 可以表示Car以及Car的父类
    public static void go(ArrayList<? extends Car> cars){}
//    public static <T extends Car> void go(ArrayList<T> cars){}
}

泛型擦除

泛型是工作在编译阶段的,一旦程序编译成class文件,class文件中就不存在泛型了,这就是泛型擦除。
泛型不支持基本数据类型,只能支持对象类型(引用数据类型)

ArrayList<int> list = new ArrayList<>(); // 报错
ArrayList<Integer> list = new ArrayList<>(); // 正确
ArrayList<double> list = new ArrayList<>(); // 报错
ArrayList<Double> list = new ArrayList<>(); // 正确

常用API

String

定义方法

String a = "123";
char[] chars = {'1', '2', '3'};
String b = new String(chars);
byte[] bytes = {97, 98, 99};
String c = new String(bytes); // 输出变量c,是"abc"

常用方法

String s = "abc";
s.length(); // 返回s的长度
s.charAt(i); // 返回下标i的元素
s.toCharArray(); // 返回一个char数组
s.equals("abc"); // 比较两个字符串
s.equalsIgnoreCase("ABC"); // 忽略大小写
s.substring(int startIndex, int endIndex); // 返回子串,左闭右开
s.substring(int startIndex); // 返回子串,从startIndex到末尾
s.replace("bc", "**"); // 将bc字符串替换成**
s.contains("123"); // 判断字符串中是否存在某个字符串
s.startsWith("ab"); // 判断字符串是否以某个字符串为开始
s.split() // 将字符串以某个字符串来分割,返回String数组

注意事项
String类型是不可变的,只要是以"..."给出的字符串对象,存储在常量池中,而且内容相同时只会存储一份

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

但是如果是用new方式创建的字符串对象,每new一次都会产生新的对象放在堆内存中

char[] chars = {'a', 'b', 'c'};
String s1 = new String(chars);
String s2 = new String(chars);
System.out.println(s1 == s2); // false

案例1

String s2 = new String("abc"); // 创建两个对象,双引号abc创建一个对象放在常量池中,new之后在堆内存中也创建一个对象
String s1 = "abc"; // 创建0个对象,因为常量池已经有abc了,所以不用创建了
System.out.println(s1 == s2); // false

案例2

String s1 = "abc"; // 存放在常量池
String s2 = "ab";
String s3 = s2 + "c"; // 运算,存放在堆内存
System.out.println(s1 == s2); // false

案例3

String s1 = "abc";
String s2 = "a" + "b" + "c"; // 在编译时会直接转化成"abc",以提高执行性能
System.out.println(s1 == s2); // true

ArrayList

ArrayList list = new ArrayList(); // 可以存放任意类型的数据
ArrayList<String> list = new ArrayList<String>(); // 指定只能存放String类型数据
list.add("123"); // 在末尾添加
list.add(1, "456"); // 在指定位置添加
list.get(1); // 获取指定位置的值
list.size(); // 返回集合中元素的个数
list.remove(1);  // 删除指定位置的元素,返回被删除的元素值
list.remove("123"); // 删除指定的元素,第一次遇到的,删除成功返回true,否则false
list.set(1, "123"); // 修改指定位置的元素值,并返回修改前的元素值

Object类

Object类是所有类的父类
public String toString() 返回对象的字符串表示形式
public boolean equals(Object o) 判断两个对象是否相等,主要是为了子类重写
protected object clone()

Objects类

Objects类是一个工具类

String s1 = "123";
String s2 = "123";
s1.equals(s2); // true
Objects.equals(s1, s2); // true
String s1 = null;
String s2 = "123";
s1.equals(s2); // 报错了
Objects.equals(s1, s2); // false

Objects.equals(a, b) 先做非空判断,再比较两个对象
Objects.isNull(obj) 判断是否为null
Objects.nonNull(obj) 与上一个相反

包装类

包装类就是把基本类型的数据包装成类

基本数据类型 对应的包装类(引用数据类型)
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean
Integer a = Integer.valueOf(12);
Integer b = 12; // 自动装箱,将int类型转成Integer类型
int c = b; // 自动拆箱,将Integer转成int

为什么要用包装类呢?
因为泛型和集合不支持基本数据类型,只能使用引用数据类型

// ArrayList<int> list = new ArrayList<>(); // 不能这么用
ArrayList<Integer> list = new ArrayList<>(); 

toString()

Integer a = 23;
String s = Integer.toString(a); // "23"
// String s = a.toString(); // 同上
// String s = a + ""; // 同上
System.out.println(s + 1); // 231

parseInt() parseDouble()

String s = "123";
String t = "99.1";
int a = Integer.parseInt(s); // 不建议用
int a = Integer.valueOf(s); // 建议用
double b = Double.parseDouble(t); // 不建议用
double b = Double.valueOf(s); // 建议用
System.out.println(a);
System.out.println(b);

StringBuilder

StringBuilder代表可变字符串对象,相当于容器,它里面装的字符串是可以改变的,
操作字符串效率高,例如拼接修改;如果字符串较少,或者不需要操作,以及定义字符串变量,建议用String

StringBuilder s = new StringBuilder("123"); // 可传可不传参数
s.append(456); // 增
s.append("789");
s.append(true);
s.charAt(0); // 查
s.setCharAt(0, 'd'); // 改
// 支持链式编程
// s.append(456).append("789").append(true);
System.out.println(s); // 123456789true
s.reverse(); // 反转字符串
s.length(); // 字符串长度
s.toString(); // 将StringBuilder转成String类型

StringBuffer

与StringBuilder的用法一模一样,但是StringBuilder是线程不安全的,StringBuffer是线程安全的。

StringJoiner

JDK8才有的,跟StringBuilder一样,是用来操作字符串的,也可以看成一个容器,创建之后里面的内容是可变的

// 构造器
StringJoiner s = new StringJoiner(", "); // 传入一个参数作为间隔符
StringJoiner s = new StringJoiner(", ", "[", "]"); // 传入三个参数,后两个分别是开始符号和结束符号
s.add("java"); // add添加数据,并返回数据本身
s.add("java").add("java"); // 链式操作
System.out.println(s); // "java, java, java"   "[java, java, java]"

Math

方法名 说明
public static int abs(int a) 绝对值
public static double ceil(double a) 向上取整
public static double floor(double a) 向下取整
public static int round(float a) 四舍五入到整数
public static int max(int a, int b) 求最大值
public static double pow(double a, double b) 求a的b次方
public static double random() 返回[0.0, 1.0)内的随机值

System

System.exit(0); 人为中断程序
System.currentTimeMillis(); 返回long类型的数,表示从1970-1-1 0:0:0开始走到此刻的毫秒值,应用例子如下

long time1 = System.currentTimeMillis();
// 程序运行过程
long time2 = System.currentTimeMillis();
System.out.println(time2 - time1); // 计算程序运行时间,单位毫秒

Runtime

代表程序所在的运行环境,Runtime是单例类,对外只提供一个对象。

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException, InterruptedException {
        Runtime r =  Runtime.getRuntime(); // 返回与当前Java应用程序关联的运行时对象
//        r.exit(0); // 终止当前运行的虚拟机
        System.out.println(r.availableProcessors()); // 虚拟机能获取的处理器数
        System.out.println(r.totalMemory() / 1024.0 / 1024.0 + "MB"); // Java虚拟机内存总量,单位字节,1024字节=1K
        System.out.println(r.freeMemory() / 1024.0 / 1024.0 + "MB"); // Java虚拟机中可用内存量
        Process p = r.exec("D:\\Microsoft VS Code\\Code.exe");// 启动某个程序,并返回代表该程序的对象
        Thread.sleep(5000); // 让程序在这里暂停5秒
        p.destroy(); // 关闭程序
    }
}

BigDecimal

解决小数运算失真的问题

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        System.out.println(a + b);
        // 1、把小数转换成字符串然后再转成BigDecimal
//        BigDecimal a1 = new BigDecimal(Double.toString(a));
//        BigDecimal b1 = new BigDecimal(Double.toString(b));
        // 推荐以下方式,更简洁
        BigDecimal a1 = BigDecimal.valueOf(a);
        BigDecimal b1 = BigDecimal.valueOf(b);
//        a1.add(b1); // 加法
//        a1.subtract(b1); // 减法
//        a1.multiply(b1); // 乘法
//        a1.divide(b1, 2, RoundingMode.HALF_UP); // 除法,保留两位小数,并四舍五入
        double a2 = a1.doubleValue(); // 将BigDecimal类型转成double类型
    }
}

时间API

JDK8之前的(不建议用)

Date

Date d = new Date(); // 创建一个Date对象,代表系统当前时间
System.out.println(d); // Mon Aug 28 15:23:44 CST 2023

long time = d.getTime(); // 获取时间毫秒值
System.out.println(time); // 1693207424931

// 把时间毫秒值转换成日期对象,2s之后是多少
time += 2000;
Date d2 = new Date(time);
System.out.println(d2); // Mon Aug 28 15:23:46 CST 2023

// 直接用setTime来修改
Date d3 = new Date();
d3.setTime(time);
System.out.println(d3); // Mon Aug 28 15:23:46 CST 2023

SimpleDateFormat

Date d = new Date(); // 创建一个Date对象,代表系统当前时间
System.out.println(d); // Mon Aug 28 15:21:54 CST 2023

long time = d.getTime(); // 获取时间毫秒值
System.out.println(time); // 1693207314195

// 格式化日期对象,和时间毫秒值
SimpleDateFormat sdf =  new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE a");
/*
y   年
M   月
d   日
H   小时
m   分
s   秒
EEE 星期几
a   上午/下午
*/
String rs = sdf.format(d);
String rs2 = sdf.format(time);
System.out.println(rs); // 2023-08-28 15:21:54 周一 下午
System.out.println(rs2); // 2023-08-28 15:21:54 周一 下午
String dateStr = "2022-12-12 12:12:12";
// 创建简单日期格式化对象,指定的时间格式必须与被解析的时间格式一模一样,否则会有bug
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf2.parse(dateStr)); // Mon Dec 12 12:12:12 CST 2022

一个应用案例

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

public class Main {
    public static void main(String[] args) throws ParseException {
        // 案例:秒杀活动,给定开始结束时间,判断某个时间是否在开始结束范围内
        String start = "2023年11月11日 0:0:0"; // 开始时间
        String end = "2023年11月11日 0:10:0"; // 结束时间
        String xj = "2023年11月11日 0:01:18"; // 时间1
        String xp = "2023年11月11日 0:10:57"; // 时间2
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        // 把字符串转换成日期对象
        Date startDt = sdf.parse(start);
        Date endDt = sdf.parse(end);
        Date xjDt = sdf.parse(xj);
        Date xpDt = sdf.parse(xp);
        // 把日期对象转换成时间毫秒值
        long startTime = startDt.getTime();
        long endTime = startDt.getTime();
        long xjTime = startDt.getTime();
        long xpTime = startDt.getTime();
        // 比较大小
        if (xjTime >= startTime && xjTime <= endTime) {
            System.out.println("小贾秒杀成功");
        } else {
            System.out.println("小贾秒杀失败");
        }
        if (xpTime >= startTime && xpTime <= endTime) {
            System.out.println("小皮秒杀成功");
        } else {
            System.out.println("小皮秒杀失败");
        }
    }
}

Calendar

Calendar now = Calendar.getInstance(); // 系统此刻时间对应的日历对象
int year = now.get(Calendar.YEAR); // 年
int days = now.get(Calendar.DAY_OF_YEAR); // 一年中的第几天
Date d = now.getTime(); // 日历中记录的日期对象
long time = now.getTimeInMillis(); // 时间毫秒值
now.set(Calendar.MONTH, 9); // 修改月份为10月
now.add(Calendar.DAY_OF_YEAR, 100); // 将日历对象中的dayofyear值加上100

JDK8之后的(建议用)

代替Date

  • Instant:时间戳 / 时间线

代替SimpleDateFormat

  • DateTimeFormatter:用于时间的格式化和解析

代替Calendar

  • LocalDate:年、月、日
  • LocalTime:时、分、秒
  • LocalDateTime:年、月、日,时、分、秒
  • ZoneId:时区
  • ZoneDateTime:带时区的时间

其他补充

  • Period:时间间隔(年、月、日)
  • Duration:时间间隔(时、分、秒、纳秒)

LocalDate

import java.time.LocalDate;

public class Main {
    public static void main(String[] args)  {
        // 获取本地日期对象
        LocalDate ld = LocalDate.now();
        System.out.println(ld);
        // 获取日期对象中的信息 调用get
        int year = ld.getYear();
        int month = ld.getMonthValue();
        int day = ld.getDayOfMonth();
        int dayOfYear = ld.getDayOfYear();
        int dayOfWeek = ld.getDayOfWeek().getValue();
        // 修改某个信息 调用with (原来日期对象并没有修改,返回了一个新的对象)
        LocalDate ld2 = ld.withYear(2099); // 修改年份
        LocalDate ld3 = ld.withMonth(12); // 修改月份
        System.out.println(ld); // 2023-08-29
        System.out.println(ld2); // 2099-08-29
        System.out.println(ld3); // 2023-12-29
        // 把某个信息加多少 调用plus
        LocalDate ld4 = ld.plusYears(2);
        LocalDate ld5 = ld.plusMonths(2);
        // 把某个信息减多少 调用minus
        LocalDate ld6 = ld.minusYears(2);
        LocalDate ld7 = ld.minusMonths(2);
        // 获取指定日期的LocalDate对象 调用of
        LocalDate ld8 = LocalDate.of(2019, 2, 2);
        LocalDate ld9 = LocalDate.of(2019, 2, 2);
        // 判断两个日期对象是否相等,在前还是在后,equals isBefore isAfter
        System.out.println(ld8.equals(ld9)); // true
    }
}

LocalTime

与LocalDate的用法几乎完全一致,只不过是获取时、分、秒、纳秒信息

LocalDateTime

包含了LocalDate和LocalTime的用法
不过LocalDateTime可以转换为LocalDate和LocalTime

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

public class Main {
    public static void main(String[] args)  {
        LocalDateTime ldt = LocalDateTime.now();
        LocalDate ld = ldt.toLocalDate();
        LocalTime lt = ldt.toLocalTime();
        LocalDateTime ldt1 = LocalDateTime.of(ld, lt);
        System.out.println(ldt); // 2023-08-29T17:12:19.007914500
        System.out.println(ld); // 2023-08-29
        System.out.println(lt); // 17:12:19.007914500
        System.out.println(ldt1); // 2023-08-29T17:12:19.007914500
    }
}

总结

方法名 示例
public static Xxxx now(): 获取系统当前时间对应的对象 LocalDate ld = LocalDate.now();
LocalDate lt = LocalTime.now();
LocalDateTime ldt = LocalDateTime.now();
public static Xxxx of(): 获取指定时间的对象 LocalDate ld = LocalDate.of(2023, 12, 12);
LocalDate lt = LocalTime.of(9, 8, 59);
LocalDateTime ldt = LocalDateTime.of(2023, 12, 12, 9, 8, 59);
方法名 说明
public int getYear() 获取年
public Month getMonth() 获取月份(January February...)
public int getMonthValue()
public int getMonth().getValue()
获取月份(1-12)
public int getDayOfMonth() 获取日
public int getDayOfYear() 获取当前是一年中的第几天
public DayOfWeek getDayOfWeek() 获取星期几(Monday Tuesday...)

ZoneId

代表时区Id

import java.time.Clock;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Set;

public class Main {
    public static void main(String[] args)  {
        ZoneId zoneId = ZoneId.systemDefault(); // 获取系统的默认时区
        System.out.println(zoneId.getId()); // Asia/Shanghai

        Set<String> zoneIdSet = ZoneId.getAvailableZoneIds(); // 获取Java 支持的全部时区Id
        System.out.println(zoneIdSet); // [Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8, ...

        ZoneId zoneId1 = ZoneId.of("America/New_York"); // 把某个时区Id封装成ZoneId对象
        ZonedDateTime now = ZonedDateTime.now(zoneId1); // 获取某个时区的ZonedDateTime对象
        System.out.println(now); // 2023-08-29T06:48:44.296055-04:00[America/New_York]

        ZonedDateTime now1 = ZonedDateTime.now(Clock.systemUTC()); // 世界标准世界,比中国要慢8小时
        System.out.println(now1); // 2023-08-29T10:48:44.298056Z

        ZonedDateTime now2 = ZonedDateTime.now(); // 无参的时候返回默认时区的时间
        System.out.println(now2); // 2023-08-29T18:48:44.298056+08:00[Asia/Shanghai]
    }
}

Instant

通过获取Instant的对象可以拿到此刻的时间,该时间由两部分组成:从1970-01-01 00:00:00开始走到此刻的总秒数+不够1秒的纳秒数(1秒=10^9纳秒)
作用:记录代码运行时间,或者用于记录某个用户操作某个事件的时间点
传统的Date类,只能精确到毫秒,并且是可变对象
新增的Instant类,可以精确到纳秒,是不可变对象,推荐用Instant代替Date

Instant now = Instant.now(); // 获取Instant对象
now.getEpochSecond(); // 获取从1970-01-01 00:00:00到现在的总秒数
now.getNano(); // 获取不够1秒的纳秒数

DateTimeFormmater

用于时间的格式化和解析

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Main {
    public static void main(String[] args)  {
        // 创建一个日期时间格式化器对象
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        // 对时间进行格式化
        LocalDateTime now = LocalDateTime.now();
        String rs = formatter.format(now);
        System.out.println(rs); // 2023-08-29 21:55:03
        // 格式化时间还有一种方案
        String rs2 = now.format(formatter);
        System.out.println(rs2);  // 2023-08-29 21:55:03
        // 解析时间:一般使用LocalDateTime提供的解析方法来解析
        String dataStr = "2029-01-01 00:00:00";
        LocalDateTime ldt = LocalDateTime.parse(dataStr, formatter);
        System.out.println(ldt); // 2029-01-01T00:00
    }
}

Period

计算两个日期间隔的年数、月数、天数

import java.time.LocalDate;
import java.time.Period;

public class Main {
    public static void main(String[] args)  {
        LocalDate start = LocalDate.of(2019, 2, 2);
        LocalDate end = LocalDate.of(2020, 1, 1);
        // 创建Period对象,封装两个日期对象
        Period period = Period.between(start, end);
        // 通过Period对象来获取两个日期对象相差的信息
        System.out.println(period.getYears()); // 间隔年份
        System.out.println(period.getMonths()); // 间隔月份
        System.out.println(period.getDays()); // 间隔天数
    }
}

Duration

计算两个时间对象相差的天数、小时数、分数、秒数、纳秒数

import java.time.Duration;
import java.time.LocalDateTime;

public class Main {
    public static void main(String[] args)  {
        LocalDateTime start = LocalDateTime.of(2020, 2, 2, 11, 10, 10);
        LocalDateTime end = LocalDateTime.of(2020, 2, 2, 11, 11, 10);
        // 创建Duration对象,封装两个日期对象
        Duration duration = Duration.between(start, end);
        // 获取两个时间对象间隔的信息
        System.out.println(duration.toDays()); // 间隔天
        System.out.println(duration.toHours()); // 间隔小时
        System.out.println(duration.toMinutes()); // 间隔分钟
        System.out.println(duration.toSeconds()); // 间隔秒
        System.out.println(duration.toMillis()); // 间隔毫秒
        System.out.println(duration.toNanos()); // 间隔纳秒
    }
}

Arrays类

介绍几个Arrays的API,toString、copyOfRange、copyOf、setAll、sort

import java.util.Arrays;
import java.util.function.IntToDoubleFunction;

public class Main {
    public static void main(String[] args)  {
        int[] arr = {1, 2, 3, 4, 5};
        System.out.println(arr); // 输出地址[I@4eec7777

        // toString 打印数组
        System.out.println(Arrays.toString(arr)); // 返回数组内容 [1, 2, 3, 4, 5]

        // copyOfRange 左闭右开
        int[] arr2 = Arrays.copyOfRange(arr, 1, 3);
        System.out.println(Arrays.toString(arr2)); // [2, 3]

        // copyOf 拷贝到一个新数组,第二个参数是新数组长度
        int[] arr3 = Arrays.copyOf(arr, 10);

        // setAll 把数组中所有值修改
        double[] prices = {60.0, 100.0, 70.1};
        Arrays.setAll(prices, new IntToDoubleFunction() {
            @Override
            public double applyAsDouble(int value) {
                return prices[value] * 0.8;
            }
        });
        System.out.println(Arrays.toString(prices)); // [48.0, 80.0, 56.08]

        // sort 排序 默认升序
        Arrays.sort(prices);
        System.out.println(Arrays.toString(prices)); // [48.0, 56.08, 80.0]
    }
}

如何将对象进行排序呢?
方法1:让对象实现Comparable接口,然后重写compareTo方法,自己制定比较规则。(自定义排序规则Comparable)
方法2:使用下面这个sort方法,创建Comparator比较器接口的匿名内部类对象,自己制定比较规则。(自定义比较器Comparator)
public static void sort(T[] arr, Comparator<? super T> c)

举例

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

public class Main {
    public static void main(String[] args)  {
        Student[] students = new Student[4];
        students[0] = new Student("小明", 2);
        students[1] = new Student("小亮", 7);
        students[2] = new Student("小红", 3);
        students[3] = new Student("小刚", 1);
        Arrays.sort(students); // 方法1
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.age - o2.age;
            }
        }); // 方法2
        System.out.println(Arrays.toString(students)); // [小刚, 小明, 小红, 小亮]
    }
}
class Student implements Comparable<Student> {
    String name;
    int age;
    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public int compareTo(Student o) {
        // 约定1:左边对象 大于 右边对象, 返回任意正整数
        // 约定2:左边对象 小于 右边对象, 返回任意负整数
        // 约定3:左边对象 等于 右边对象, 返回0
//        if (this.age > o.age) {
//            return 1;
//        } else if (this.age < o.age) {
//            return -1;
//        }
//        return 0;
        return this.age - o.age; // 升序
//        return o.age - this.age; // 降序
    }
    @Override
    public String toString() {
        return this.name;
    }
}

JDK8新特性

Lambda表达式

Lambda表达式是JDK8新增的一种语法形式,作用:用于简化匿名内部类的代码写法
只能简化函数式接口的匿名内部类。
什么是函数式接口?有且仅有一个抽象方法的接口

public class Main {
    public static void main(String[] args)  {
        Swimming s1 = new Swimming() {
            @Override
            public void swim() {
                System.out.println("匿名内部类,学生游泳");
            }
        };
        Swimming s2 = () -> {
            System.out.println("Lambda表达式,学生游泳");
        };
        s1.swim();
        s2.swim();
    }
}
interface Swimming {
    void swim();
}

像前文提到的Arrays.setAll(),也可以用Lambda表达式来改写

import java.util.Arrays;
import java.util.function.IntToDoubleFunction;

public class Main {
    public static void main(String[] args) {
        // setAll 把数组中所有值修改
        double[] prices = {60.0, 100.0, 70.1};
//        Arrays.setAll(prices, new IntToDoubleFunction() {
//            @Override
//            public double applyAsDouble(int value) {
//                return prices[value] * 0.8;
//            }
//        }); // 匿名类
        Arrays.setAll(prices, (int value) -> {
            return prices[value] * 0.8;
        }); // Lambda表达式来简化匿名内部类
        System.out.println(Arrays.toString(prices)); // [48.0, 80.0, 56.08]
    }
}

Arrays.sort()用Lambda改写

        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                if (o1.age > o2.age) {
                    return 1;
                } else if (o1.age < o2.age) {
                    return -1;
                }
                return 0;
            }
        }); // 匿名内部类
        Arrays.sort(students, (Student o1, Student o2) -> {
            if (o1.age > o2.age) {
                return 1;
            } else if (o1.age < o2.age) {
                return -1;
            }
            return 0;
        }); // Lambda表达式

Lambda省略规则

  • 参数类型可以不写
  • 如果只有一个参数,参数类型可以省略,同时()也要省略
  • 如果Lambda表达式中的方法体代码只有一行代码,可以省略大括号,同时必须去掉分号,如果这行代码是return的,也必须去掉return

方法引用

静态方法的引用

类名::静态方法
如果某个Lambda表达式里只是调用一个静态方法,并且前后参数的形式一致,就可以使用静态方法引用

实例方法的引用

对象名::实例方法
如果某个Lambda表达式里只是调用一个实例方法,并且前后参数的形式一致,就可以使用实例方法引用

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

public class Main {
    public static void main(String[] args)  {
        Student[] students = new Student[4];
        students[0] = new Student("小明", 2);
        students[1] = new Student("小亮", 7);
        students[2] = new Student("小红", 3);
        students[3] = new Student("小刚", 1);
        // 原始写法,对学生类别按照年龄排序
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.age - o2.age;
            }
        });
        // Lambda简化写法
        Arrays.sort(students, (o1, o2) -> o1.age - o2.age);

        // Lambda表达式调用一个静态方法
        Arrays.sort(students, (o1, o2) -> CompareByData.compareByAge(o1, o2));
        // 静态方法引用
        Arrays.sort(students, CompareByData::compareByAge);

        // Lambda表达式调用一个实例方法
        CompareByData compare = new CompareByData();
        Arrays.sort(students, (o1, o2) -> compare.compareByAgeDesc(o1, o2));
        // 实例方法引用
        Arrays.sort(students, compare::compareByAgeDesc);

        System.out.println(Arrays.toString(students));
    }
}
class Student {
    String name;
    int age;
    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return this.name;
    }
}

class CompareByData {
    public static int compareByAge(Student o1, Student o2){ // 静态方法,可以通过类直接调用
        return o1.age - o2.age; // 升序
    }
    public int compareByAgeDesc(Student o1, Student o2){ // 必须实例化对象后通过对象调用
        return o2.age - o2.age; // 降序
    }
}

特定类型方法的引用

类型::方法
如果某个Lambda表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,则此时就可以使用特定类型方法的引用

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

public class Main {
    public static void main(String[] args) {
        String[] names = { "boby","angela","Andy", "dlei" ,"caocao","Babo","jack", "Cici"};
        // 忽略大小写排序
        Arrays.sort(names, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.compareToIgnoreCase(o2); // 忽略大小写比较
            }
        });
        // Lambda改写
        Arrays.sort(names, (o1, o2) -> o1.compareToIgnoreCase(o2));
        // 特定类型的方法引用
        Arrays.sort(names, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(names));
    }
}

构造器的引用

类名::new
如果某个Lambda表达式里只是在创建对象,并且前后参数情况一致,就可以使用构造器引用

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

public class Main {
    public static void main(String[] args) {
        // 创建匿名内部类对象
//        CreateCar cc = new CreateCar() {
//            @Override
//            public Car create(String name, double price) {
//                return new Car(name, price);
//            }
//        };
        // 用Lambda表达式简化
//        CreateCar cc = (name, price) -> new Car(name, price);
        // 构造器引用
        CreateCar cc = Car::new;
        Car c = cc.create("奔驰", 49.9);
        System.out.println(c);

    }
}
interface CreateCar{
    Car create(String name, double price);
}
class Car {
    String name;
    double price;
    Car(String name, double price){
        this.name = name;
        this.price = price;
    }
}

Stream流

用于操作集合或者数组的数据

获取Stream流

// 1、调用List、Set的Stream方法
List<String> names = new ArrayList<>();
// Set<String> names = new HashSet<>();
Collections.addAll(names, "张三", "李四", "王五");
Stream<String> stream = names.stream();

// 2、调用Map的Stream方法
Map<String, Double> map = new HashMap<>();
map.put("张三", 1.0);
map.put("李四", 2.0);

Set<String> keys = map.keySet();
Stream<String> ks = keys.stream(); // map的key的stream流

Collection<Double> values = map.values();
Stream<Double> vs = values.stream(); // map的value的stream流

Set<Map.Entry<String, Double>> entries = map.entrySet(); // map的键值对集合
Stream<Map.Entry<String, Double>> kvs = entries.stream(); // 键值对流
kvs.filter(e -> e.getKey().startsWith("张")).forEach(e -> System.out.println(e.getKey() + ", " + e.getValue()));

// 3、获取数组的Stream
String[] names2 = {"张三", "李四", "王五"};
Stream<String> s1 = Arrays.stream(names2);
Stream<String> s2 = Stream.of(names2);

Stream流常见的中间方法

List<Double> scores = new ArrayList<>();
Collections.addAll(scores, 88.5, 100.0, 71.5, 90.5, 40.0, 30.0);
// 需求1,找出成绩大于等于60分的数据,升序
scores.stream().filter(s -> s >= 60).sorted().forEach(System.out::println);
List<Student> students = new ArrayList<>();
students.add(new Student("张三", 26, 185.0));
students.add(new Student("张三", 50, 190.0));
students.add(new Student("李四", 27, 160.0));
students.add(new Student("王五", 16, 170.0));
// 需求2,找出年龄在[23, 30]的,降序
students.stream().filter(s -> s.age >= 23 && s.age <= 30).sorted((o1, o2) -> o2.age - o1.age).forEach(System.out::println);
// 需求3,找出身高前3高的,limit(n)取前n个
students.stream().sorted((o1, o2) -> Double.compare(o2.height, o1.height)).limit(3).forEach(System.out::println);
// 需求4,找出身高倒数2名,skip(n)跳过前n个
students.stream().sorted((o1, o2) -> Double.compare(o1.height, o2.height)).limit(2).forEach(System.out::println);
students.stream().sorted((o1, o2) -> Double.compare(o2.height, o1.height)).skip(students.size() - 2).forEach(System.out::println);
// 需求5,找出身高超过175的学生的名字,要求去除重复名字, map() 将对象用name来代替,然后用distinct() 去重
students.stream().filter(s -> s.height > 175).map(s -> s.name).distinct().forEach(System.out::println);
// concat 合并两个Stream
Stream<String> s1 = Stream.of("张三", "李四");
Stream<String> s2 = Stream.of("王五", "刘能");
Stream.concat(s1, s2).forEach(System.out::println);

Stream流的终结方法

forEach()
count()
max()
min()
collect(Collectors.toList()) 返回一个List
collect(Collectors.toSet()) 返回一个Set
collect(Collectors.toMap(a -> a.name, a -> height)) 用name作键,height作值,存入Map
toArray() 存到数组中

正则表达式(没学)

用来校验数据格式是否合法

异常

运行时异常:RuntimeException及其子类(如数组越界)
编译时异常:编译阶段就会出现错误提醒
抛出异常(throws)
在方法上使用throws关键字,可以将方法中的异常抛出去给调用者处理

方法 throws 异常1, 异常2, 异常3...{
	...
}

捕获异常(try...catch)
直接捕获程序出现的异常

try{
	// 监视可能出现异常的代码
}catch(异常类型1 变量){
	// 处理异常
}catch(异常类型2 变量){
	// 处理异常
}...

自定义运行时异常
1、定义一个异常类继承RuntimeException
2、重写构造器
3、通过throw new异常类(xxx)来创建异常对象并抛出。

public class ExceptionTest {
	public static void main(String[] args) {
		try{
			f(1);
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("底层出现了bug");
		}
	}
	public static void f(int a) {
		if (a > 0) {
			System.out.println("正确");
		} else {
			// 3、通过throws new异常类(xxx)来创建异常对象并抛出。
			throw new MyRuntimeException("a非法");
		}
	}
}
// 1、定义一个异常类继承RuntimeException
public class MyRuntimeException extends RuntimeException {
	// 2、重写构造器
	public MyRuntimeException() {}
	public MyRuntimeException(String message) {
		super(message);
	}
}

自定义编译时异常
1、定义一个异常类继承Exception
2、重写构造器
3、通过throw new异常类(xxx)来创建异常对象并抛出。

集合框架

Collection

常用方法

方法名 说明
public boolean add(E e) 把给定的对象添加到当前集合中
public void clear() 清空集合中所有的元素
public boolean remove(Ee) 把给定的对象在当前集合中删除
public boolean contains(object obj) 判断当前集合中是否包含给定的对象
public boolean isEmpty() 判断当前集合是否为空
public int size() 返回集合中元素的个数
public object[ ] toArray() 把集合中的元素,存储到数组中

遍历方式

迭代器

Collection<String> c = new ArrayList<>();
c.add("java1");
c.add("java2");
Iterator it = c.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

增强for

既可以遍历集合,也可以遍历数组

Collection<String> c = new ArrayList<>();
c.add("java1");
c.add("java2");
for (String s: c) {
    System.out.println(s);	
}

Lambda表达式

forEach()方法

Collection<String> c = new ArrayList<>();
c.add("java1");
c.add("java2");
c.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
}); // 匿名内部类
// 改写成Lambda表达式
c.forEach(s -> System.out.println(s));
// 用方法引用
c.forEach(System.out::println);

List集合

List系列集合特点:有序、可重复、有索引
List的两个实现类ArrayList、LinkedList也具有上面三个特点

ArrayList

基于数组实现

LinkedList

基于双链表实现

Set集合

Set系列集合特点:无序、不重复、无索引
HashSet:无序、不重复、无索引
LinkedHashSet:有序、不重复、无索引
TreeSet:排序、不重复、无索引

HashSet

基于哈希表实现,jdk8之后:数组+链表+红黑树

LinkedHashSet

基于哈希表实现,但是每个元素都多了个双链表机制来记录前后元素的顺序

TreeSet

基于红黑树实现排序
利用TreeSet对Student对象排序的方法
Set set = new TreeSet<>((o1, o2) -> o1.age - o2.age);

可变参数

是一种特殊的形参,定义在方法、构造器的形参列表里,格式是:数据类型...参数名称
特点:可以不传数据给它、可以传一个或者同时传多个,也可以传一个数组。
好处:可以用来灵活的接受数据。
注意:可变参数在方法内部就是一个数组,一个形参列表中可变参数只能有一个,可变参数必须放在形参列表的最后面。

public class Main {
    public static void main(String[] args) {
        test();
        test(10);
        test(1,2,3);
    }
    public static void test(int...nums) {
        System.out.println(nums.length);
        System.out.println(Arrays.toString(nums));
    }
}

Map

常用方法

Map<String, Integer> map = new HashMap<>();
map.put(key, value)
map.size()
map.clear()
map.isEmpty()
map.get(key)
map.remove(key)
map.containsKey(key)
map.containsValue(value)
Set<String> key = map.keySet(); // 获取全部键的集合
Collection<Integer> values = map.values(); // 获取全部values值
map1.putAll(map2); // 把map2添加到map1中

HashMap

无序、不重复、无索引
Map<String, Integer> map = new HashMap<>();
map.put("手表", 100);
map.get("手表");

LinkedHashMap

有序、不重复、无索引

TreeMap

自动根据键来排序、不重复、无索引

集合嵌套

Map<String, List<String>> map = new HashMap<>();
List<String> cities1 = new ArrayList<>();
Collections.addAll(cities1, "南京", "苏州");
map.put("江苏省", cities1);
List<String> cities2 = new ArrayList<>();
Collections.addAll(cities2, "杭州", "宁波");
map.put("浙江省", cities2);
System.out.println(map);

Deque双端队列

import java.util.Deque;
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        /*Deque本质是双端队列*/
        /* 头-------------尾*/
        Deque<Integer> deque = new LinkedList<>();
        deque.offerFirst(3); // 头增
        deque.offerLast(2); // 尾增
        deque.pollFirst(); // 头删,如果为空,返回null
        deque.pollLast(); // 尾删,如果为空,返回null
        deque.peekFirst(); // 头查,如果为空,返回null
        deque.peekLast(); // 尾查,如果为空,返回null
        deque.contains(1); // 判断是否存在某个元素
        deque.isEmpty(); // 判断是否为空
        
        /*如果只当做普通队列用的话,用下面的方法,就不用指定First还是Last了*/
        deque.offer(1);
        deque.poll();
        deque.peek();

        /*如果当做栈,用下面的方法*/
        deque.push(1);
        deque.pop();
        deque.peek();

    }
}

IO流

File:操作文件
IO流:读写数据

常用方法

在这里插入图片描述
File对象既可以代表文件、也可以代表文件夹。
File封装的对象仅仅是一个路径名,这个路径可以是存在的,也允许是不存在的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

案例1:文件搜索

需求:从D盘中搜索QQ.exe这个文件,找到后输出其位置,并启动QQ

import java.io.File;

public class FileTest1 {
    public static void main(String[] args) throws Exception {
        searchFile(new File("D:/"), "QQ.exe");
    }

    /**
     * 去目录下搜索某个文件
     * @param dir 目录
     * @param fileName 要搜索的目录名称
     */
    public static void searchFile(File dir, String fileName) throws Exception {
        if (dir == null || !dir.exists() || dir.isFile()) {
            return;
        }
        File[] files = dir.listFiles();
        if (files != null && files.length > 0) {
            for (File f : files) {
                if (f.isFile()) {
                    if (f.getName().equals(fileName)) {
                        System.out.println("找到了: " + f.getAbsolutePath());
                        // 把QQ启动
                        Runtime runtime = Runtime.getRuntime();
                        runtime.exec(f.getAbsolutePath());
                    }
                } else {
                    searchFile(f, fileName);
                }
            }
        }
    }
}

案例2:删除指定的非空文件夹

分析:先把目标文件夹下的内容都删掉,再删除文件夹

import java.io.File;

public class FileTest1 {
    public static void main(String[] args) throws Exception {
        File dir = new File("E:\\java\\javase\\data");
        deleteDir(dir);
    }
    public static void deleteDir(File dir) {
        if (dir == null || !dir.exists()) {
            return;
        }
        if (dir.isFile()) { // 如果是文件,那就直接删除
            dir.delete();
            return;
        }
        // dir是文件夹,需要获得里面的一级文件对象
        File[] files = dir.listFiles();
        if (files == null) { // null表示没有访问权限
            return;
        }
        if (files.length == 0) { // 文件夹为空直接删除再返回
            dir.delete();
            return;
        }
        // 此时dir是个有内容的文件夹,
        for (File f : files) {
            if (f.isFile()) {
                f.delete();
            } else {
                deleteDir(f);
            }
        }
        dir.delete();
    }
}

字符集、编码、解码

ASCII字符集:只有英文、数字、符号等,占1个字节。
GBK字符集:汉字占2个字节,英文、数字占1个字节。
UTF-8字符集:汉字占3个字节,英文、数字占1个字节。

import java.util.Arrays;

public class FileTest1 {
    public static void main(String[] args) throws Exception {
        String data = "a我b";
        byte[] bytes = data.getBytes(); // 编码,默认用utf-8
        System.out.println(Arrays.toString(bytes)); // [97, -26, -120, -111, 98]
        byte[] bytes1 = data.getBytes("GBK"); // 用GBK编码
        System.out.println(Arrays.toString(bytes1)); // [97, -50, -46, 98]
        String s1 = new String(bytes); // 解码,默认用utf-8
        System.out.println(s1);
        String s2 = new String(bytes1, "GBK"); // 用GBK解码
        System.out.println(s2);
    }
}

IO流

字节输入流 InputStream(读字节数据的)
字节输出流 OutoutStream(写字节数据出去的)
字符输入流 Reader(读字符数据的)
字符输出流 Writer(写字符数据出去的)
字节流适合复制文件,不适合读写文本文件
字符流适合读写文本文件内容

IO流-字节流

FileInputStream FileOutputStream
案例:复制文件

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class FileTest1 {
    public static void main(String[] args) throws Exception {
        InputStream is = new FileInputStream("E:\\java\\javase\\data\\图片1.png");
        OutputStream os = new FileOutputStream("E:\\java\\javase\\data\\图片2.png");
        byte[] buffer = new byte[1024]; // 1KB
        int len; // 记录每次读取了多少个字节
        while ((len = is.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }
        os.close();
        is.close();
        System.out.println("复制完成");
    }
}

字节流非常适合做一切文件的复制操作,因为任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题

释放资源的方式

try-catch-finally

try {
	...
} catch (IOException e) {
	e.printStackTrace();
} finally {
	...
}
// 复制文件案例的try-catch-finally
import java.io.*;

public class FileTest1 {
    public static void main(String[] args) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream("E:\\java\\javase\\data\\图片1.png");
            os = new FileOutputStream("E:\\java\\javase\\data\\图片2.png");
            byte[] buffer = new byte[1024]; // 1KB
            int len; // 记录每次读取了多少个字节
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            System.out.println("复制完成");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (os != null) os.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

finally代码区的特点:无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止。
作用:一般用于在程序执行完成后进行资源的释放操作(专业级做法)。

try-with-resource

JDK7提供的更简单的资源释放方案

try(定义资源1;定义资源2;...) {
	可能出现的异常;
} catch(异常类名 变量名) {
	异常处理的代码;
}
import java.io.*;

public class FileTest1 {
    public static void main(String[] args) {
        try (
                InputStream is = new FileInputStream("E:\\java\\javase\\data\\图片1.png");
                OutputStream os = new FileOutputStream("E:\\java\\javase\\data\\图片2.png");
                // 注意这里只能放资源
                // 资源一般指的是最终实现了AutoCloseable接口
                ) {
            byte[] buffer = new byte[1024]; // 1KB
            int len; // 记录每次读取了多少个字节
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            System.out.println("复制完成");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

IO流-字符流

FileReader FileWriter

import java.io.*;

public class FileTest1 {
    public static void main(String[] args) {
        try (
                Reader fr = new FileReader("E:\\java\\javase\\data\\a.txt");
                ) {
            char[] buffer = new char[3]; // 每次读3个字符
            int len;
            while ((len = fr.read(buffer)) != -1) {
                System.out.println(new String(buffer, 0, len));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
import java.io.*;

public class FileTest1 {
    public static void main(String[] args) throws Exception {
        try (
                Writer fw = new FileWriter("E:\\java\\javase\\data\\b.txt");
                // 第二个参数为true表示在后面追加写入数据
                ) {
            fw.write('a');
            fw.write("\n");
            fw.write("b");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        
    }
}

IO流-缓冲流

缓冲流自带了8KB缓冲池,提高读写数据的性能

字节缓冲流

InputStream is = new FileInputStream("file.txt");
InputStream bis = new BufferedInputStream(is);
OutputStream os = new FileOutputStream("file.txt");
OutputStream bos = new BufferedOutputStream(os);

字符缓冲流

Reader fr = new FileReader("file.txt");
BufferedReader br = new BufferedReader(fr);
br.readLine(); // 字符缓冲流新增的,读取一行数据,没有数据就返回null
Writer fw = new FileWriter("file.txt");
BufferedWriter bw = new BufferedWriter(fw);
bw.newLine();

案例:拷贝出师表到另一个文件,恢复顺序

分析: 定义一个缓存字符输入流管道与源文件接通。
定义一个List集合存储读取的每行数据。
定义一个循环按照行读取数据,存入到List集合中去。
对List集合中的每行数据按照首字符编号升序排序。
定义一个缓存字符输出管道与目标文件接通。
遍历List集合中的每个元素,用缓冲输出管道写出并换行。

3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐托付不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
import java.io.*;
import java.util.*;

public class FileTest1 {
    public static void main(String[] args) {
        try (
                BufferedReader br = new BufferedReader(new FileReader("E:\\java\\javase\\data\\出师表.txt"));
                BufferedWriter bw = new BufferedWriter(new FileWriter("E:\\java\\javase\\data\\出师表2.txt"));
                ) {
            List<String> data = new ArrayList<>();
            String line;
            while ((line = br.readLine()) != null) {
                data.add(line);
            }
            // 将每行前面的序号分割出来然后排序
            data.sort(Comparator.comparingInt(o -> Integer.parseInt(o.split("\\.")[0])));
            System.out.println(data);
            for (String ln : data) {
                bw.write(ln);
                bw.newLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

IO流-转换流

字符输入转换流

InputStreamReader
解决不同编码时,字符流读取文本内容乱码的问题
解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了

import java.io.*;

public class FileTest1 {
    public static void main(String[] args) {
        try (
                // 原始方法,由于gbk.txt是GBK编码的,所以会出现乱码
//                BufferedReader br = new BufferedReader(new FileReader("src/gbk.txt"));
                // 用字符输入转换流解决乱码问题
                InputStream is = new FileInputStream("src/gbk.txt"); // 先获取原始字节流
                Reader isr = new InputStreamReader(is, "GBK"); // 将其按真实的字符集编码转成字符输入转换流
                BufferedReader br = new BufferedReader(isr); // 然后将字符输入转换流转成字符缓冲流
                ) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

字符输出转换流

OutputStreamWriter

import java.io.*;

public class FileTest1 {
    public static void main(String[] args) {
        try (
                OutputStream os = new FileOutputStream("gbk.txt");
                Writer osw = new OutputStreamWriter(os, "GBK");
                BufferedWriter bw = new BufferedWriter(osw);
                ) {
            bw.write("我是中国人");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

IO流-打印流

import java.io.*;

public class FileTest1 {
    public static void main(String[] args) {
        try (
                PrintStream ps = new PrintStream("itheima.txt");
                ) {
            ps.println(1);
            ps.println("我爱你");
            ps.println(1.2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

PrintStream和PrintWriter差不多
应用:输出重定向,主要用System.setOut(ps)

import java.io.*;
public class FileTest1 {
    public static void main(String[] args) {
        System.out.println("1");
        System.out.println("2");
        try (
                PrintStream ps = new PrintStream("itheima.txt");
                ) {
            System.setOut(ps); // 后面的输出就输出到文件中了
            System.out.println("3");
            System.out.println("4");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

IO流-数据流

DataInputStream DataOutputStream
DataOutputStream允许把数据和其类型一并写出去。
DataInputStream用于读取数据输出流写出去的数据

import java.io.*;
public class FileTest1 {
    public static void main(String[] args) {
        try (
                DataOutputStream dos = new DataOutputStream(new FileOutputStream("itheima.txt"));
                DataInputStream dis = new DataInputStream(new FileInputStream("itheima.txt"));
                ) {
            dos.writeInt(97);
            dos.writeDouble(1.0);
            System.out.println(dis.readInt()); // 先写的int,就得先读int
            System.out.println(dis.readDouble());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

IO流-序列化流

ObjectOutputStream(对象字节输出流),可以把Java对象进行序列化:把Java对象存入到文件中去。
ObjectInputStream(对象字节输入流),可以把Java对象进行反序列化:把存储在文件中的Java对象读入到内存中来。

public class User implements Serializable { 
	// 注意:对象如果要参与序列化,必须实现序列化接口(java.io.Serializable)
    private String loginName;
    private String userName;
    private int age;
    private transient String passWord; 
    // 加入transient关键字后,这个变量就不参与序列化了
}
import java.io.*;
public class Test {
    public static void main(String[] args) {
        try (
                ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); // 进行序列化
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); // 进行反序列化
                ) {
            User u = new User("admin", "张三", 32, "12345678");
            // 序列化对象到文件中
            oos.writeObject(u);
            System.out.println("序列化对象成功");
            User user = (User) ois.readObject(); // User{loginName='admin', userName='张三', age=32, passWord='12345678'}
            System.out.println(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

IO流体系图

FileInputStream 字节输入流
FileOutputStream 字节输出流
FileReader 字符输入流
FileWriter 字符输出流
BufferedInputStream 字节缓冲输入流
字符输出转换流
OutputStreamWriter

在这里插入图片描述

特殊文件

Properties

是一个Map集合(键值对集合),但是我们一般不会当集合使用。
核心作用:Properties是用来代表属性文件的,通过Properties可以读写属性文件里的内容。

读取properties文件

在这里插入图片描述

import java.io.FileReader;
import java.util.Properties;
import java.util.Set;

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建一个Properties对象出来
        Properties properties = new Properties();
        System.out.println(properties);
        properties.load(new FileReader("E:\\java\\javase\\src\\users.properties"));
        System.out.println(properties);
        Set<String> keys = properties.stringPropertyNames(); // 获取全部键的集合
        for (String key : keys) {
            String value = properties.getProperty(key);
            System.out.println(key + ", " + value);
        }
        properties.forEach((key, value) -> {
            System.out.println(key + ", " + value);
        });
    }
}

写入properties文件

在这里插入图片描述

import java.io.FileWriter;
import java.util.Properties;

public class Test {
    public static void main(String[] args) throws Exception { 
        Properties properties = new Properties();
        properties.setProperty("张无忌", "minmin");
        properties.setProperty("殷素素", "cuishan");
        properties.setProperty("张翠山", "susu");
        properties.store(new FileWriter("E:\\java\\javase\\src\\users2.properties"), "I saved many users");
    }
}

修改properties文件

import java.io.FileReader;
import java.io.FileWriter;
import java.util.Properties;

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建一个Properties对象出来
        Properties properties = new Properties();
        // 读取文件
        properties.load(new FileReader("E:\\java\\javase\\src\\users.properties"));
        if (properties.containsKey("张无忌")) {
            properties.setProperty("张无忌", "xiaozhao");
        }
        properties.store(new FileWriter("E:\\java\\javase\\src\\users.properties"), "success");
    }
}

XML(没学)

XML(全称EXtensible Markup Language,可扩展标记语言)

日志(没学)

多线程

多线程创建

方法一:继承Thread类

1、定义子类MyThread,继承java.lang.Thread,重写run方法
2、创建MyThread对象
3、调用线程对象的start方法启动线程(启动后执行run方法)

// 线程的创建方式一: 继承Thread类
public class Main {
    public static void main(String[] args) { // main方法默认是由一条主线程负责执行
        Thread t = new MyThread(); // 3、创建MyThread线程类的对象代表一个线程
        t.start(); // 4、启动线程,自动执行run方法
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程main: " + i);
        }
    }
}

class MyThread extends Thread{ // 1、继承Thread类
    @Override
    public void run() { // 2、重写run方法
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程MyThread: " + i);
        }
    }
}

优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展

注意:

  • 启动线程必须用start,不能用run
  • 不要把主线程放在启动子线程之前

方法二:实现Runnable接口

1、定义一个线程任务类,实现Runnable接口,重写run方法
2、创建任务对象
3、把任务对象交给Thread处理
4、start启动

public class Main {
    public static void main(String[] args) {
        // 3、创建任务对象
        Runnable target = new MyRunnable();
        // 4、把任务对象交给线程对象
        Thread thread = new Thread(target);
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:" + i);
        }
    }
}

class MyRunnable implements Runnable { // 1、定义一个任务类,实现Runnable接口
    // 2、重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:" + i);
        }
    }

}

优缺点:

  • 优点:任务类只是实现接口,还可以继承其他类、实现其他接口,扩展性更强
  • 缺点:多创建一个Runnable对象(也谈不上缺点)

方法二的匿名内部类的写法

Runnable target = new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:" + i);
        }
    }
};

方法三:实现Callable接口

前两种线程创建方式都存在一个问题,run方法无法返回值,所以Callable解决了这个问题
1、创建任务对象

  • 定义类实现Callable接口,重写call方法,封装要做的事,和要返回的数据
  • 把Callable类型的对象封装成FutureTask(线程任务对象)

2、把线程任务对象交给Thread对象
3、调用Thread对象的start方法启动线程
4、线程执行完毕后,通过FutureTask对象的get方法获取线程任务执行的结果

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

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> call = new MyCallable(5); // 3、创建Callable对象
        FutureTask<Integer> f1 = new FutureTask<>(call); // 4、把Callable对象封装成一个FutureTask对象(任务对象)
        // FutureTask的作用:FuTureTask实现了Runnable对象,可以在线程执行完毕后,用get获取结果
        new Thread(f1).start(); // 5、把任务对象交给Thread对象
        int sum = f1.get();
        System.out.println(sum);
    }
}

class MyCallable implements Callable<Integer> { // 1、实现Callable接口
    int n;
    public MyCallable(int n) {this.n = n;}

    @Override
    public Integer call() { // 2、重写Call方法
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return sum;
    }
}

优缺点

  • 优点:线程任务类只是实现接口,可以继承类和实现接口,扩展性强;可以在线程执行完毕后获取线程执行结果
  • 缺点:编码复杂一点

Thread常用方法

Thread提供的常用方法 说明
public void run() 线程的任务方法
public void start() 启动线程
public String getName() 获取当前线程名字
public void setName(String name) 为线程设置名字
public static Thread currentThread() 获取当前执行的线程对象
public static void sleep(long time) 让当前线程睡眠多少毫秒后再执行
public final void join() 让调用这个方法的线程执行完
Thread提供的常见构造器 说明
public Thread(String name) 指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target, String name) 封装Runnable对象成为线程对象,并指定线程名称

线程安全

多个线程,同时访问同一个共享资源,且存在修改该资源。

线程同步

利用线程同步来解决线程安全问题,思想是让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

同步代码块

作用:把访问共享资源的核心代码块给上锁,以此保证线程安全

synchronized(同步锁) {
	访问共享资源的核心代码
}

原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
同步锁注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug

同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全

修饰符 synchronized 返回值类型 方法名称(形参列表) {
	访问共享资源的核心代码
}

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

底层原理:同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。如果方法是实例方法,同步方法默认用this作为锁的对象。如果方法是静态方法,同步方法默认用类名.class作为锁的对象。

Lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

final Lock lk = new ReentrantLock();
lk.lock();
访问共享资源的核心代码
lk.unlock();

通常以下操作,即使核心代码有bug,也可以解锁

final Lock lk = new ReentrantLock();
try {
	访问共享资源的核心代码
} catch (Exception e) {
	e.printStackTrace();
} finally {
	lk.lock();
}

线程通信

生产者消费者案例

import java.util.*;
public class Main {
    public static void main(String[] args){
        Desk desk = new Desk();
        new Thread(() -> {
            while (true)
                desk.put();
        }, "厨师1").start();
        
        new Thread(() -> {
            while (true)
                desk.put();
        }, "厨师2").start();
        
        new Thread(() -> {
            while (true)
                desk.put();
        }, "厨师3").start();

        new Thread(() -> {
            while (true) {
                desk.get();
            }
        }, "吃货1").start();

        new Thread(() -> {
            while (true) {
                desk.get();
            }
        }, "吃货2").start();
    }
}
class  Desk {
    List<String> list = new ArrayList<>();

    public synchronized void put() {
        try {
            String name = Thread.currentThread().getName();
            // 判断是否有包子
            if (list.size() == 0) {
                list.add(name + "做的包子");
                System.out.println(name + "做了个包子");
                Thread.sleep(2000);
            }
            this.notifyAll(); // 唤醒别人
            this.wait(); // 等待自己
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void get() {
        try {
            String name = Thread.currentThread().getName();
            if (list.size() == 1) {
                System.out.println(name + "吃了" + list.get(0));
                list.clear();
                Thread.sleep(1000);
            }
            this.notifyAll();
            this.wait();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

线程池

创建线程池

方法一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
方法二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

corePoolSize:指定线程池的核心线程的数量
maximumPoolSize:指定线程池的最大线程数量
keepAliveTime:指定临时线程的存活时间
unit:指定临时线程存活的时间单位(秒、分、时、天)
workQueue:指定线程池的任务队列
threadFactory:指定线程池的线程工厂
handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

ExecutorService核心方法

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行Runnable任务
Future<T> submit(Callable<T> task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务
void shutdown() 等任务执行完毕后关闭线程池
List<Runnable> shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

线程池处理Runnable任务

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args){
        // 创建3个核心线程,最大线程数是5,线程池的任务队列可以容纳4个,临时线程5-3=2
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        Runnable target = new MyRunnable();
        // 前3个进入核心线程
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 中间4个进入任务队列
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 最后2个进入临时线程
        pool.execute(target);
        pool.execute(target);
        // 再来新任务就要拒绝了
        pool.execute(target); // 抛异常

        pool.shutdown(); // 等线程池的任务全部执行完,再关闭线程池
        pool.shutdownNow(); // 代码执行到这里时,立即关闭线程池

    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "输出666");
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

线程池处理Callable任务

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建3个核心线程,最大线程数是5,线程池的任务队列可以容纳4个,临时线程5-3=2
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}
class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) { this.n = n; }
    @Override
    public String call() {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
    }
}

Executors工具类实现线程池

创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它:

public static ExecutorService newFixedThreadPool(int nThreads)

创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程:

public static ExecutorService newSingleThreadExecutor()

线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉:

public static ExecutorService newCachedThreadPool()

创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

这些方法的底层,都是通过线程池的实现类ExecutorService创建的线程池对象

并发、并行

正在运行的程序(软件)就是一个独立的进程。
线程是属于进程的,一个进程中可以同时运行很多个线程。
进程中的多个线程其实是并发和并行执行的。

并发
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行
在同一时刻,同时有多个线程在被CPU调度执行

线程生命周期

也就是线程从生到死的过程中,经历的各种状态及状态转换。
Java一共定义了6种状态,在Thread类的内部枚举类中
在这里插入图片描述

线程状态 说明
NEW(新建) 线程刚被创建,但是并未启动
Runnable(可运行) 线程已经调用了start(),等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

网络通信

基本通信架构
CS架构(Client客户端/Server服务端)
BS架构(Browser浏览器/Server服务端)

网络通信三要素

IP地址:设备在网络中的地址,是唯一的标识
端口:应用程序在设备中的唯一标识
协议:连接和数据在网络中传输的规则

IP地址

特殊IP地址
127.0.0.1、localhost:代表本机IP,只会寻找当前所在的主机
InetAddress
代表IP地址,常用方法如下

名称 说明
public static InetAddress getLocalHost() 获取本机IP
public static InetAddress getByName(String host) 根据Ip地址或域名,返回一个InetAddress对象
public String getHostName() 获取该IP地址对象对应的主机名
public String getHostAddress() 获取该IP地址对象中的ip地址信息
public boolean isReachable(int timeout) 在指定毫秒内,判断主机与该ip对应的主机是否能连通
import java.net.InetAddress;

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取本机IP地址对象的主机名和地址信息
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println(ip1.getHostName());
        System.out.println(ip1.getHostAddress());
        // 获取指定IP或者域名的IP地址对象
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ip2.getHostName());
        System.out.println(ip2.getHostAddress());
        System.out.println(ip2.isReachable(6000));
    }
}

端口号

端口标记正在计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535
分类
周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占80,FTP占21)
注册端口:1024~49151,分配给用户进程或某些应用程序
动态端口:49152~65535,一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序一般使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。

协议

网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。

  • OSI网络参考模型:全球网络互联标准
  • TCP/IP网络模型:事实上的国际标准
    在这里插入图片描述
    UDP(User Datagram Protocol): 用户数据报协议;TCP(Transmission Control Protocol): 传输控制协议

UDP协议

  • 特点:无连接、不可靠通信。
  • 不事先建立连接,数据按照包发,一包数据包含:自己的IP、程序端口、目的地IP、程序端口和数据(限制在64KB内)等。
  • 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。
  • 通信效率高,可用于语音通话,视频直播

TCP协议

  • 面向连接、可靠通信。
  • TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。
  • TCP主要有三个步骤实现可靠传输:三次握手建立可靠连接传输数据进行确认四次挥手断开连接
  • 通信效率相对不高,用于网页、文件下载、支付

1、三次握手,目的是确定通信双方收发消息都是正常无问题的,可以建立可靠连接
(1)客户端 发送连接请求
(2)服务端 返回一个响应
(3)客户端 再次发出确认信息,连接建立

2、传输数据进行确认,目的是确保数据传输的可靠性。

3、四次挥手断开连接,目的是确保双方数据的收发都已经完成
(1)客户端 发出断开连接请求
(2)服务端 返回一个响应:稍等(因为服务端可能还没接受完数据呢)
(3)服务端 返回一个响应:确认断开(服务端将数据处理完后就说可以断开啦)
(4)客户端 发出正式确认断开连接

UDP通信

DatagramSocket:用于创建客户端、服务端

public DatagramSocket() 创建客户端的Socket对象,系统会随机分配一个端口号
public DatagramSocket(int port) 创建服务端的Socket对象,并指定端口号
public void send(DatagramPacket dp) 发送数据包
public void receive(DatagramPacket p) 使用数据包接受数据

DatagramPacket:创建数据包

public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发出去的数据包对象
public DatagramPacket(byte[] buf, int length) 创建用来接受数据的数据包
public int getLength() 获取数据包,实际接收到的字节个数
// Server.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class Server {
    public static void main(String[] args) throws Exception{
        System.out.println("服务端启动");
        // 创建一个服务端对象,注册端口
        DatagramSocket socket = new DatagramSocket(6666);
        // 创建一个数据包对象,用于接受数据
        byte[] buffer = new byte[1024 * 64]; // 64KB
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        // 接受客户端发来的数据
        socket.receive(packet);
        System.out.println("服务端接受数据");
        int len = packet.getLength(); // 从字节数组中把接收到的数据打印出来,接受多少就倒出多少
        String rs = new String(buffer, 0, len); // 转成String
        System.out.println(rs);
        System.out.println(packet.getAddress().getHostAddress());
        System.out.println(packet.getPort());
    }
}
// Client.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class Client {
    public static void main(String[] args) throws Exception {
        // 创建客户端对象
        DatagramSocket socket = new DatagramSocket(7777);
        // 创建数据包对象封装要发出去的数据
        byte[] bytes = "我是客户端".getBytes();
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
                InetAddress.getLocalHost(), 6666);
        // 开始正式发送这个数据包的数据
        socket.send(packet);
        System.out.println("客户端数据发送完毕");
        // 释放资源
        socket.close();
    }
}

TCP通信

  • 客户端程序是通过java.net包下的Socket类来实现的
public Socket(String host, int port) 根据指定的服务器ip,端口号请求与服务端建立连接,连接通过,就获得了客户端socket
public OutputStream getOutputStream() 获得字节输出流对象
public InputStream getInputStream() 获得字节输入流对象
  • 服务端是通过java.net包下的ServerSocket类来实现的
// Client.java
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws Exception {
        // 创建Socket对象,并同时请求与服务器程序的连接
        Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(), 8888);
        // 使用socket对象调用getOutputStream()得到一个字节输出流
        OutputStream os = socket.getOutputStream();
        // 把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);
        // 写数据出去
        dos.writeUTF("客户端发的消息");
        // 释放资源
        dos.close();
        socket.close();
    }
}

// Server.java
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception{
        // 创建ServerSocket对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
        // 使用ServerSocket对象,调用accept方法,等待客户端的连接请求
        Socket socket = serverSocket.accept();
        // 从socket通信管道中得到一个字节输入流
        InputStream is = socket.getInputStream();
        // 把原始的字节输入流包装成数据输入流
        DataInputStream dis = new DataInputStream(is);
        // 使用数据输入流读取客户端发送的消息
        String rs = dis.readUTF();
        System.out.println(rs);
        // 也可以获取客户端的IP地址
        System.out.println(socket.getRemoteSocketAddress());
        dis.close();
        socket.close();
    }
}

若要实现多个客户端通信,必须要用多线程

// Client.java
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws Exception {
        // 创建Socket对象,并同时请求与服务器程序的连接
        Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(), 8888);
        // 使用socket对象调用getOutputStream()得到一个字节输出流
        OutputStream os = socket.getOutputStream();
        // 把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请说");
            String msg = sc.nextLine();
            if ("exit".equals(msg)) {
                System.out.println("欢迎您使用");
                dos.close();
                socket.close();
                break;
            }
            dos.writeUTF(msg);
            dos.flush();
        }
    }
}
// Server.java
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception{
        // 创建ServerSocket对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
        while (true) {
            // 使用ServerSocket对象,调用accept方法,等待客户端的连接请求
            Socket socket = serverSocket.accept();
            System.out.println(socket.getRemoteSocketAddress() + "上线了");
            // 把这个客户端对应的socket通信管道,交给一个独立的 线程处理
            new ServerReadThread(socket).start();
        }
    }
}
// ServerReadThread.java
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;

public class ServerReadThread extends Thread{
    private Socket socket;
    public ServerReadThread(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            while (true) {
                try {
                    String msg = dis.readUTF();
                    System.out.println(msg);
                } catch (Exception e) {
                    System.out.println("离线了");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TCP通信-端口转发

Java高级

单元测试

针对最小的功能单元(方法),编写测试代码对其进行正确性测试。

Junit单元测试框架

  • 将Junit框架的jar包导入到项目中(IDEA集成了Junit框架,不需要我们自己手工导入)
  • 为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
  • 测试方法上必须声明@Test注解,然后在测试方法中,编写代码调用被测试的业务方法进行调试
  • 开始测试:选中测试方法,右键选择“JUnit运行”,如果测试通过则是绿色,失败是红色。
package com.itheima.d1_junit;
import org.junit.Assert;
import org.junit.Test;
public class StringUtilTest {
    @Test // 测试方法
    public void testPrintNumber() {
        StringUtil.printNumber("admin");
        StringUtil.printNumber(null);
    }
    @Test // 测试方法
    public void testGetMaxIndex() {
        int index1 = StringUtil.getMaxIndex("admin");
        System.out.println(index1);
        // 断言机制
        Assert.assertEquals("方法内部有bug", 4, index1);
    }
}
注解 说明
@Test 测试类中的方法必须用它修饰才能成为测试方法,才能启动执行
@Before 用来修饰一个实例方法,该方法会在每一个测试方法前执行
@After 用来修饰一个实例方法,该方法会在每一个测试方法后执行
@BeforeClass 用来修饰一个静态方法,该方法会在所有测试方法前只执行一次
@AfterClass 用来修饰一个静态方法,该方法会在所有测试方法后只执行一次

开始执行的方法:初始化资源
执行完之后的方法:释放资源

反射

反射就是:加载类,并允许以编程的方式解剖类中的各种成分(成员变量、方法、构造器)

步骤

1、 加载类,获取类的字节码:Class对象

Class c1 = Student.class; // 获取Class对象
System.out.println(c1.getName()); // 全类名
System.out.println(c1.getSimpleName()); // 简名 Student
System.out.println(c1.getMethods());

2、获取类的构造器

Class c1 = Student.class;
Constructor[] constructors = c1.getConstructors(); // 获取public修饰的构造器
Constructor[] constructors = c1.getDeclaredConstructors(); // 获取所有声明的构造器
Constructor constructor = c1.getConstructor(); // 获取无参构造器
Constructor constructor = c1.getConstructor(String.class, int.class); // 获取有参构造器
// 打印构造器的名字以及参数数目
System.out.println(constructor.getName() + ", " + constructor.getParameterCount());

获取类构造器的作用:初始化对象返回 newInstance()

Class c1 = Student.class;
Constructor constructor = c1.getConstructor(); // 获取无参构造器
constructor.setAccessible(true); // 禁止检查访问权限
Student student = (Student) constructor.newInstance(); // 初始化对象

3、获取类的成员变量

Class c = Student.class;
Field[] fields = c.getDeclaredFields(); // 获取声明的(private protected public)成员变量
for (Field field : fields) {
    System.out.println(field.getName() + ", " + field.getType());
}

获取成员变量的作用:赋值、取值 set() get()

Class c = Student.class;
Field fName = c.getDeclaredField("name"); // 获取name成员变量
// 赋值
Student student = new Student();
fName.setAccessible(true); // 禁止检查访问权限
fName.set(student, "小名"); // 为student对象的name成员变量赋值为“小名”
System.out.println(student);
// 取值
String name = (String) fName.get(student);
System.out.println(name);

4、获取类的成员方法

Class c = Student.class;
Method[] methods = c.getDeclaredMethods();
Method run = c.getDeclaredMethod("run");
Student student = new Student();
run.invoke(student);

反射的作用

得到一个类的全部成分然后操作
破坏封装性
设计框架

注解

注解就是Java代码里的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息来决定怎么执行程序

自定义注解

public @interface 注解名称 {
	public 属性类型 属性名() default 默认值;
}

注解的原理

在这里插入图片描述

  • 注解本质是一个接口,Java中所有注解都是继承了Annotation接口
  • @注解(...):其实就是一个实现类对象,实现了该注解以及Annotation接口

元注解

元注解指的是:修饰注解的注解
在这里插入图片描述

注解的解析

什么是注解的解析?
就是判断类上、方法上、成员变量上是否存在注解,并把注解里的内容给解析出来
如何解析注解?
指导思想:要解析谁上面的注解,就应该先拿到谁
比如要解析类上面的注解,则应该现获取该类的Class对象,再通过Class对象解析其上面的注解
比如要解析成员方法上的注解,则应该获取到该成员方法的Method对象,再通过Method对象解析其上面的注解
Class、Method、Field、Constructor都实现了AnnotatedElement接口,它们都拥有解析注解的能力

// MyTest.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) // 元注解,声明注解的保留周期
@Target({ElementType.TYPE, ElementType.METHOD}) // 声明注解在类、方法中使用
public @interface MyTest {
    String value();
    double aaa() default 100;
    String[] bbb();
}
// Demo.java

@MyTest(value = "类", aaa = 99.1, bbb = {"3", "4"}) // 注解修饰类
public class Demo {
    @MyTest(value = "方法", aaa = 99.4, bbb = {"1", "2"}) // 注解修饰方法
    public void test1() {
    }
}
// AnnotationTest.java

import org.junit.Test;

import java.lang.reflect.Method;
import java.util.Arrays;

public class AnnotationTest {
    @Test
    public void parseClass() {
        // 1、先得到Class对象
        Class c = Demo.class;
        // 2、解析类上的注解
        if (c.isAnnotationPresent(MyTest.class)) {
            // 判断c这个对象是否存在MyTest这个注解
            MyTest myTest = (MyTest) c.getDeclaredAnnotation(MyTest.class); // 解析类上的注解
            System.out.println(myTest.value());
            System.out.println(myTest.aaa());
            System.out.println(Arrays.toString(myTest.bbb()));
        }
    }
    @Test
    public void parseMethod() throws Exception {
        // 1、先得到Class对象
        Class c = Demo.class;
        Method m = c.getDeclaredMethod("test1");
        // 2、解析类上的注解
        if (m.isAnnotationPresent(MyTest.class)) {
            // 判断c这个对象是否存在MyTest这个注解
            MyTest myTest = (MyTest) m.getDeclaredAnnotation(MyTest.class); // 解析方法上的注解
            System.out.println(myTest.value());
            System.out.println(myTest.aaa());
            System.out.println(Arrays.toString(myTest.bbb()));
        }
    }
}

动态代理(没学)