Java Note three

day18 API

1.Math

常见方法介绍

Math.PI
public static int abs(int a)                    // 返回参数的绝对值
public static double ceil(double a)             // 向上取整,向数轴的正方向进1,返回大于或等于参数的最小整数,结果更大(负数同理-22.8->-22)
public static double floor(double a)            // 向下取整,,向数轴的负方向进1,返回小于或等于参数的最大整数,结果更小(负数同理)
public static int max(int a,int b)              // 获取两个int值中的较大值
public static int min(int a,int b)              // 获取两个int值中的较小值

public static double sqrt(double a)              // 计算a的平方根
public static double cbrt(double a)              // 计算a的立方根
public static int round(float a)                // 按照四舍五入返回最接近参数的int类型的值
public static double random()                   // 返回一个[0.0,1.0)的随机值  0.859329692171231
public static double pow (double a,double b)     // 计算a的b次幂的值

Math.abs

// Math.bug:
// int类型取值范围 -2147483648 - 2147483647
// 如果没有正数与负数对应,那么传递负数结果有误
System.out.println(Math.abs(-2147483648));  // -2147483648
System.out.println(Math.abs(-2147483647));  // 2147483647

System.out.println(Math.absExact(-2147483648)); // 报错

Math.round(负数)

假如里面是负数,则情况不太一样,如Math.round(-22.5),按四舍五入应该是-23,但结果是22。-22.6结果就是-23。

个人感觉:有点像是给数值+0.5后向下取整

System.out.println("==============================");
System.out.println(Math.round(-22.4));          // -22
System.out.println(Math.round(-22.4999999));    // -22
System.out.println(Math.round(-22.5));           // -22
System.out.println(Math.round(-22.5000001));    // -23
System.out.println(Math.round(-22.6));         // -23

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

Math.pow

Math.pow(4, 0.5);  // 2.0
Math.pow(2, -2); // 0.25

猜质数:具体看笔记1,

核心内容就是:从2开始,到 被猜数 (这里一定是等于)的平方根 Math.sqrt(),for循环。

for (int i = 2; i <= Math.sqrt(number); i++) // 这里是 <= ,不然49的结果就是 质数。

2.System

常见方法介绍

我们要学习的System类中的常见方法如下所示:

public static long currentTimeMillis()          // 获取当前时间所对应的毫秒值,用于计算一段代码运行时长(当前时间为0时区所对应的时间即就是英国格林尼治天文台旧址所在位置)
public static void exit(int status)             // 终止当前正在运行的Java虚拟机,0表示正常退出,非零表示异常退出
public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length); // 进行数值元素copy

代码

long start = System.currentTimeMillis();  // long 型
System.out.println(start);  // 1678808047061

System.exit(0);

实例1:演示arraycopy方法

方法参数说明:

// src:      源数组
// srcPos:  源数值的开始位置
// dest:    目标数组
// destPos: 目标数组开始位置
// length:   要复制的元素个数
public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length); 

使用这个方法我们也可以完成数组元素的删除操作,如下所示:

public class SystemDemo02 {
    public static void main(String[] args) {
        // 定义一个数组
        int[] srcArray = {23 , 45 , 67 , 89 , 14 , 56 } ;
        // 删除数组中第3个元素(67):要删除67这个元素,我们只需要将67后面的其他元素依次向前进行移动即可
        System.arraycopy(srcArray , 3 , srcArray , 2 , 3);

        // 因为我们是在原数组上复制移动,所以最后一位还会存在。结果将是:23, 45, 89, 14, 56, 56  
        // 将最后一个位置的元素设置为0
        srcArray[srcArray.length - 1] = 0 ;

        // 遍历srcArray数组
        System.out.println(Arrays.toString(srcArray));
        // =======================================================
        for(int x = 0 ; x < srcArray.length ; x++) {
            if(x != desArray.length - 1) {
                System.out.print(srcArray[x] + ", ");
            }else {
                System.out.println(srcArray[x]);
            }
        }
    }
}

arraycopy方法底层细节:

1.如果数据源数组和目的地数组都是基本数据类型,那么两者的类型必须保持一致,否则会报错(int类型拷贝不到double类型的数组里)

2.在拷贝的时候需要考虑数组的长度,如果超出范围也会报错(一个值都赋值不上)

3.如果数据源数组和目的地数组都是引用数据类型,那么子类类型可以赋值给父类类型

3.Runtime

Runtime表示Java中运行时对象,可以获取到程序运行时设计到的一些信息

常见方法:

public static Runtime getRuntime()      //当前系统的运行环境对象,只能有一个对象Runtime.getRuntime()
public void exit(int status)            //停止虚拟机,System.exit的底层源码
public int availableProcessors()        //获得CPU的线程数
public long maxMemory()                 //JVM能从系统中获取总内存大小(单位byte)
public long totalMemory()               //JVM已经从系统中获取总内存大小(单位byte)
public long freeMemory()                //JVM剩余内存大小(单位byte)
public Process exec(String command)     //运行cmd命令

代码示例:

public class RunTimeDemo1 {
    public static void main(String[] args) throws IOException {
        /*
            public static Runtime getRuntime() 当前系统的运行环境对象
            public void exit(int status) 停止虚拟机
            public int availableProcessors() 获得CPU的线程数
            public long maxMemory() JVM能从系统中获取总内存大小(单位byte)
            public long totalMemory() JVM已经从系统中获取总内存大小(单位byte)
            public long freeMemory() JVM剩余内存大小(单位byte)
            public Process exec(string command) 运行cmd命令
        */

        //1.获取Runtime的对象
        //Runtime r1 =Runtime.getRuntime();

        //2.exit 停止虚拟机
        //Runtime.getRuntime().exit(0);
        //System.out.println("看看我执行了吗?");

        //3.获得CPU的线程数
        System.out.println(Runtime.getRuntime().availableProcessors());//8
        //4.总内存大小,单位byte字节
        System.out.println(Runtime.getRuntime().maxMemory() / 1024 / 1024);//4064  // 2002
        //5.已经获取的总内存大小,单位byte字节
        System.out.println(Runtime.getRuntime().totalMemory() / 1024 / 1024);//254  // 126
        //6.剩余内存大小
        System.out.println(Runtime.getRuntime().freeMemory() / 1024 / 1024);//251  // 123

        //7.运行cmd命令
        //shutdown :关机
        //加上参数才能执行
        //-s :默认在1分钟之后关机
        //-s -t 指定时间 : 指定关机时间
        //-a :取消关机操作
        //-r: 关机并重启
        Runtime.getRuntime().exec("shutdown -s -t 3600");

        // 可操作,不可显示,输入java -version是没有可见输出结果的,但下面这个可以
        Runtime.getRuntime().exec("javac F:\\MyCode\\new-java\\java-learn-3\\src\\main\\java\\test\\Test.java");
    }
}

4.Object

Object类所在包是java.lang包。Object 是类层次结构的根,每个类都可以将 Object 作为超类。所有类都直接或者间接的继承自该类;换句话说,该类所具备的方法,其他所有类都继承了。

object

常见方法介绍

我们要学习的Object类中的常见方法如下所示:

//返回该对象的字符串表示形式(可以看做是对象的内存地址值)javabean常客
public String toString()    // 和直接sout(对象)是一样的。

//比较两个对象地址值是否相等;true表示相同,false表示不相同
public boolean equals(Object obj)
protected Object clone()                //对象克隆

案例演示

  1. toString()源码
public String toString() {      // Object类中toString方法的源码定义
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

// com.itheima.api.system.demo04.Student@3f3afe78  对象的内存地址值
// 一般改写成:Student{name='george', age='18'}

小结:

  1. 在通过输出语句输出一个对象时,默认调用的就是toString()方法
  2. 输出地址值一般没有意义,我们可以通过重写toString方法去输出对应的成员变量信息
  3. toString方法的作用:以良好的格式,更方便的展示对象中的属性值
  4. 一般情况下Jdk所提供的类都会重写Object类中的toString方法
  5. equals方法的源码
public boolean equals(Object obj) {     // Object类中的equals方法的源码
    return (this == obj);
}
String s = "abc";
StringBuilder sb = new StringBuilder("abc");

System.out.println(s.equals(sb));  // false,  String类的equals,不是String类直接false
System.out.println(sb.equals(s));  // false,  StringBuilder类没有equals,按Object类的地址比较

默认情况下equals方法比较的也是对象的地址值。比较内存地址值一般情况下是没有意义的,我们希望比较的是对象的属性,如果两个对象的属性相同,我们认为就是同一个对象;

那么要比较对象的属性,我们就需要在Student类中重写Object类中的equals方法。alt + insert

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return age == student.age && Objects.equals(name, student.name);
}  // 比较的是对象的name属性值和age属性值

// hashCode方法我们暂时使用不到,可以将hashCode方法删除。
@Override
public int hashCode() {
    return 0;
}

4.1 对象克隆

  • 把A对象的属性值完全拷贝给B对象,也叫对象拷贝,对象复制

对象克隆的分类:

深克隆和浅克隆

浅克隆:

  • 不管对象内部的属性是基本数据类型还是引用数据类型,都完全拷贝过来
  • 基本数据类型拷贝过来的是具体的数据,引用数据类型拷贝过来的是地址值
  • Object类默认的是浅克隆

细节:

  • 方法在底层会帮我们创建一个对象,并把原对象中的数据拷贝过去。

书写细节:

  1. 重写Object中的clone方法(clone方法是protect的)
  2. 让javabean类实现Cloneable接口
  3. 创建原对象并调用clone就可以了User u2 =(User)u1.clone();

浅克隆

深克隆:

  • 基本数据类型拷贝过来,字符串复用,引用数据类型会重新创建新的
  • 亲测,System.arrayCopy是深克隆

深克隆

代码实现:

package com.itheima.a04objectdemo;

public class ObjectDemo4 {
    public static void main(String[] args) throws CloneNotSupportedException {
        // protected object clone(int a) 对象克隆 

        //1.先创建一个对象
        int[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0};
        User u1 = new User(1, "zhangsan", "1234qwer", "girl11", data);

        //2.克隆对象
        User u2 =(User)u1.clone();

        //验证一件事情:Object中的克隆是浅克隆
        //想要进行深克隆,就需要重写clone方法并修改里面的方法体
        int[] arr = u1.getData();
        arr[0] = 100;

        System.out.println(u1);
        System.out.println(u2);// u1值修改影响u2

        //以后一般会用第三方工具进行克隆
        //1.第三方写的代码导入到项目中,导入gson.jar进入项目lib文件夹,右键Add as Library...
        //2.编写代码
        //Gson gson =new Gson();
        //把对象变成一个字符串
        //String s=gson.toJson(u1);
        //再把字符串变回对象就可以了
        //User user =gson.fromJson(s, User.class);

        //int[] arr=u1.getData();
        //arr[0] = 100;

        //打印对象
        //System.out.println(user);   // 深克隆

    }
}
// ================================================================
package com.itheima.a04objectdemo;

import java.util.StringJoiner;

//Cloneable接口,没有抽象方法
//如果一个接口里面没有抽象方法
//表示当前的接口是一个标记性接口
//现在Cloneable表示一旦实现了,那么当前类的对象就可以被克降
//如果没有实现,当前类的对象就不能克隆
public class User implements Cloneable {
    private int id;
    private String username;
    private String password;
    private String path;
    private int[] data;

    public User() {
    }

    public User(int id, String username, String password, String path, int[] data) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.path = path;
        this.data = data;
    }

    // 这里省略5个 getter setter

    public String toString() {
        return "角色编号为:" + id + ",用户名为:" + username + "密码为:" + password + ", 游戏图片为:" + path + ", 进度:" + arrToString();
    }

    public String arrToString() {
        StringJoiner sj = new StringJoiner(", ", "[", "]");

        for (int i = 0; i < data.length; i++) {
            sj.add(data[i] + "");
        }
        return sj.toString();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //调用父类中的clone方法
        //相当于让Java帮我们克隆一个对象,并把克隆之后的对象返回出去。
        //return super.clone();  // 默认浅克隆

        //先把被克隆对象中的数组获取出来
        int[] data = this.data;
        //创建新的数组
        int[] newData =new int[data.length];
        //拷贝数组中的数据
        for (int i = 0; i < data.length; i++) {
            newData[i] = data[i];
        }
        //调用父类中的方法克隆对象
        User u=(User)super.clone();
        //因为父类中的克隆方法是浅克隆,替换克隆出来对象中的数组地址值
        u.data =newData;
        return u;
    }
}

5.Objects类

  • Objects类所在包是在java.util包下,因此在使用的时候需要进行导包。并且Objects类是被final修饰的,因此该类不能被继承
  • Objects类中无无参构造方法,因此我们不能使用new关键字去创建Objects的对象。同时我们可以发现Objects类中所提供的方法都是静态的。因此我们可以通过类名直接去调用这些方法。
  • Objects类提供了一些对象常见操作的方法。比如判断对象是否相等,判断对象是否为null等等。

上面代码示例的Object.equal,即使自动重写了equals方法,如果调用者本身为null,及(null→)s1.equals(s2)会报空指针异常

常见方法介绍

我们要重点学习的Objects类中的常见方法如下所示:

public static String toString(Object o)                     // 获取对象的字符串表现形式,如果javabean类没有重写,返回的还是地址值
public static boolean equals(Object a, Object b)            // 先做非空判断,比较两个对象是否相等
public static boolean isNull(Object obj)                    // 判断对象是否为null
public static boolean nonNull(Object obj)                   // 判断对象是否不为null

Objects.equals源码:(多了调用者null判断

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

// 如果是基本类型,直接判断值,引用类型则判断地址值
// 增加判断调用者是不是null,
// a.equals(b),这里如果我们是两个Student类判断,如果重写了equals,则用重写后的规则进行判断,否则还是判断地址值。

Objects.toString()

相比Object的方法多了null判断

// Student student = new Student("george", 18);  // 下面输出结果一样为地址
Student student = null;

System.out.println(student);  // null
System.out.println(student.toString());   // 这里空指针报错
System.out.println(Objects.toString(student));  // null

我们要了解的Objects类中的常见方法如下所示:

public static <T> T requireNonNull(T obj)                 // 检查对象是否不为null,如果为null直接抛出异常;如果不是null返回该对象;(地址值)
public static <T> T requireNonNullElse(T obj, T defaultObj) // 检查对象是否不为null,如果不为null,返回该对象;如果为null返回defaultObj值
public static <T> T requireNonNullElseGet(T obj, Supplier<? extends T> supplier)    // 检查对象是否不为null,如果不为null,返回该对象;如果                                                            // 为null,返回由Supplier所提供的值

上述方法中的T可以理解为是Object类型。

代码示例:

public class ObjectsDemo02 {

    public static void main(String[] args) {

        // 调用方法
        method_03();

    }

    // 演示requireNonNullElseGet
    public static void method_03() {

        // 创建一个学生对象
        Student s1 = new Student("itheima" , "14") ;

        // 调用Objects对象的requireNonNullElseGet方法,该方法的第二个参数是Supplier类型的,查看源码我们发现Supplier是一个函数式接口,
        // 那么我们就可以为其传递一个Lambda表达式,而在Supplier接口中所定义的方法是无参有返回值的方法,因此具体调用所传入的Lambda表达式如下所示
        Student student = Objects.requireNonNullElseGet(s1, () -> {
            return new Student("itcast", "14");
        });

        // 输出
        System.out.println(student);

    }

    // 演示requireNonNullElse
    public static void method_02() {

        // 创建一个学生对象
        Student s1 = new Student("geore" , "14") ;

        // 调用Objects对象的requireNonNullElse方法
        Student student = Objects.requireNonNullElse(s1, new Student("wahson", "14"));

        // 输出
        System.out.println(student);

    }

    // 演示requireNonNull
    public static void method_01() {

        // 创建一个学生对象
        Student s1 = new Student("itheima" , "14") ;

        // 调用Objects对象的requireNonNull方法
        Student student = Objects.requireNonNull(s1);

        // 输出
        System.out.println(student);

    }

}

6.BigInteger

平时在存储整数的时候,Java中默认是int类型,int类型有取值范围:-2147483648 ~ 2147483647。如果数字过大,我们可以使用long类型,但是如果long类型也表示不下怎么办呢?

就需要用到BigInteger,可以理解为:大的整数。有多大呢?理论上最大到42亿的21亿次方。基本上在内存撑爆之前,都无法达到这个上限。

6.1 常见方法

构造方法

public BigInteger(int num, Random rnd)      //获取随机大整数,范围:[0 ~ 2的num次方-1]
public BigInteger(String val)               //获取指定的大整数,String类型整数!
public BigInteger(String val, int radix)    //获取指定进制的大整数

下面这个不是构造,而是一个静态方法获取BigInteger对象
public static BigInteger valueOf(long val)  //静态方法获取BigInteger的对象,内部有优化

构造方法小结:

  • 如果BigInteger表示的数字没有超出long的范围,可以用静态方法获取。
  • 如果BigInteger表示的超出long的范围,可以用构造方法获取。
  • 对象一旦创建,BigInteger内部记录的值不能发生改变。
  • 只要进行计算都会产生一个新的BigInteger对象
BigInteger b1 = new BigInteger(10, new Random());  // [0, 2 ^ 10 -1]
System.out.println(b1);

// BigInteger b2 = new BigInteger("9.9"); // 小数报错:NumberFormatException,同理"abc"
BigInteger b2 = new BigInteger("-99999999"); // 可以负数
System.out.println(b2);

// 1.字符串必须整数
// 2.字符串的数字必须和进制吻合
BigInteger b3 = new BigInteger("100", 2);  // 2进制的100,结果为4
// BigInteger b3 = new BigInteger("123", 2);  // 2进制却有123,NumberFormatException
BigInteger b4 = new BigInteger("100", 3);  // 3进制的100,结果为9
// BigInteger b5 = new BigInteger("10.1", 2);  // NumberFormatException

// 静态方法获取,参数为long,范围没上面大
BigInteger b6 = BigInteger.valueOf(10000);  //
BigInteger b7 = BigInteger.valueOf(Long.MAX_VALUE);  // 9223372036854775807
BigInteger b8 = BigInteger.valueOf(Long.MAX_VALUE + 2);  // -9223372036854775807 !
// BigInteger b8 = BigInteger.valueOf(9223372036854775808L);  报错

// 在内部对常用的数字: -16 ~ 16进行了优化,提前把-16 ~ 16创建好BigInteger的对象,多次获取不会创建新的。
BigInteger bb1 = BigInteger.valueOf(16);  // 如果是17,超出范围,为false
BigInteger bb2 = BigInteger.valueOf(16);
System.out.println(bb1 == bb2);  // true

// 对象一旦创建内部的数据不能发生改变
BigInteger bd1 = BigInteger.valueOf(1);
BigInteger bd2 = BigInteger.valueOf(2);
BigInteger result = bd1.add(bd2);
System.out.println(result == bd1); // false
System.out.println(result == bd2); // false

常见成员方法

BigDecimal类中使用最多的还是提供的进行四则运算的方法,如下:

public BigInteger add(BigInteger val)                   //加法
public BigInteger subtract(BigInteger val)              //减法
public BigInteger multiply(BigInteger val)              //乘法
public BigInteger divide(BigInteger val)                //除法
public BigInteger[] divideAndRemainder(BigInteger val)   //除法,获取商和余数
public  boolean equals(Object x)                        //比较是否相同
public  BigInteger pow(int exponent)                    //次幂、次方
public  BigInteger max/min(BigInteger val)              //返回较大值/较小值
public  int intValue(BigInteger val)                    //转为int类型整数,超出范围数据有误

代码实现:

BigInteger bd1 = BigInteger.valueOf(100);
BigInteger bd2 = BigInteger.valueOf(32);

System.out.println(bd1.add(bd2));  // 132
System.out.println(bd1.subtract(bd2));  // 68
System.out.println(bd1.multiply(bd2));  // 3200
System.out.println(bd1.divide(bd2));   // 3

// 商 和 余数
BigInteger[] arr = bd1.divideAndRemainder(bd2);
System.out.println(arr[0]);   // 商 3
System.out.println(arr[1]);   // 余数 4

System.out.println(bd1.equals(100));  // false 类型不一样,int 和 BigInteger
System.out.println(bd1.equals(new BigInteger("100")));  // true

// bd1.pow(bd2);  // 报错,参数为int类型
System.out.println(bd1.pow(2));  // 10000

System.out.println(bd1.max(bd2));    // 100
System.out.println(bd1.min(bd2));    // 32
System.out.println(bd1 == (bd1.max(bd2))); // true  返回原对象

int i = bd1.intValue();
System.out.println(i);  // 100

long l = bd1.longValue();
double v = bd1.doubleValue();
System.out.println(l);  // 100
System.out.println(v);  // 100.0

6.2底层存储方式

对于计算机而言,其实是没有数据类型的概念的,都是0101010101,数据类型是编程语言自己规定的,所以在实际存储的时候,先把具体的数字变成二进制,每32个bit为一组,存储在数组中。

数组中最多能存储元素个数:int的最大值,21亿多

数组中每一位能表示的数字:(-2147483648 ~2147486347)42亿多

理论上,BigInteger能表示的最大数字为:42亿的21亿次方。

但是还没到这个数字,电脑的内存就会撑爆,所以一般认为BigInteger是无限的。

存储方式如图所示:

bi

bigInteger的底层原理

7.BigDecimal类

7.1 引入

System.out.println(0.09 + 0.01);  // 0.09999999999999999
System.out.println(0.216 - 0.1);  // 0.11599999999999999
System.out.println(0.226 * 0.01);  // 0.0022600000000000003
System.out.println(0.09 / 0.1);  // 0.8999999999999999

这样的结果其实就是一个丢失精度的结果。为什么会产生精度丢失呢?

在使用float或者double类型的数据在进行数学运算的时候,很有可能会产生精度丢失问题。我们都知道计算机底层在进行运算的时候,使用的都是二进制数据;

当我们在程序中写了一个十进制数据 ,在进行运算的时候,计算机会将这个十进制数据转换成二进制数据,然后再进行运算,计算完毕以后计算机会把运算的结果再转换成十进制数据给我们展示; 如果我们使用的是整数类型的数据进行计算,那么在把十进制数据转换成二进制数据的时候不会存在精度问题;

如果我们的数据是一个浮点类型的数据,有的时候计算机并不会将这个数据完全转换成一个二进制数据,而是将这个将其转换成一个无限的趋近于这个十进数的二进制数据; 这样使用一个不太准确的数据进行运算的时候, 最终就会造成精度丢失;

计算机中的小数

计算机的小数1

计算机小数2

0.875刚刚好可以用111,而假如是0.9,0.226就可能有n位了。超出部分被舍弃。

double

为了提高精度,Java就给我们提供了BigDecimal供我们进行数据运算。

7.2 概述

BigDecimal所在包是在java.math包下,因此在使用的时候就需要进行导包。我们可以使用BigDecimal类进行更加精准的数据计算。

7.3常见方法

构造方法

要用BigDecimal类,那么就需要首先学习一下如何去创建BigDecimal的对象。通过查看API文档,我们可以发现Jdk中针对BigDecimal类提供了很多的构造方法,但是最常用的构造方法是:

BigDecimal

下面这个不是构造,而是一个静态方法获取BigInteger对象
public static BigDecimal valueOf(double val)    //静态方法获取BigDecimal的对象,内部有优化

public BigDecimal(double val)

API 文档:此构造方法的结果有一定的不可预知性。有人可能认为在 Java 中写入 new BigDecimal(0.1) 所创建的 BigDecimal 正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为 0.1 无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入 到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。

另一方面,String 构造方法是完全可预知的,通常建议优先使用 String 构造方法。

BigDecimal b1 = new BigDecimal(0.09);  // 类型非String
BigDecimal b2 = new BigDecimal(0.01);  // Unpredictable不可预知的 'new BigDecimal()' call
// 结果 0.09999999999999999687749774324174723005853593349456787109375
System.out.println(b1.add(b2));
  1. 如果表示的数字不大,没有超出double的取值范围,建议使用静态valueOf()方法
  2. 如果数字比较大,建议构造方法
  3. 如果我们传递的是0-10之间的整数(10.0就不行),包括0和10,那么静态valueOf()方法会返回已经创建好的对象,不会重新创建
BigDecimal bd1 = BigDecimal.valueOf(10);
BigDecimal bd2 = BigDecimal.valueOf(10);
System.out.println(bd1 == bd2);   // true
BigDecimal bd3 = new BigDecimal("10");
System.out.println(bd3 == bd1);  // false

System.out.println(BigDecimal.valueOf(11) == BigDecimal.valueOf(11));  // false
System.out.println(BigDecimal.valueOf(10.0) == BigDecimal.valueOf(10.0));  // false
System.out.println(BigDecimal.valueOf(10) == BigDecimal.valueOf(10));  // true

常见成员方法

BigDecimal类中使用最多的还是提供的进行四则运算的方法,如下:

public BigDecimal add(BigDecimal value)             // 加法运算
public BigDecimal subtract(BigDecimal value)        // 减法运算
public BigDecimal multiply(BigDecimal value)        // 乘法运算
public BigDecimal divide(BigDecimal value)          // 除法运算
public BigDecimal divide(BigDecimal value, 精确几位,舍入模式)           // 除法运算
BigDecimal b1 = new BigDecimal("0.01");
BigDecimal b2 = new BigDecimal("0.03");

System.out.println(b1.add(b2));
System.out.println(b1.subtract(b2));
System.out.println(b1.multiply(b2));
//        System.out.println(b1.divide(b2)); // 报错ArithmeticException,非终止十进制扩展;没有确切的可表示的十进制结果
System.out.println(BigDecimal.valueOf(10.0).divide(BigDecimal.valueOf(2.0)));  // 5

System.out.println(b1.divide(b2, 2, RoundingMode.HALF_UP));  // 结果保留2位,四舍五入 0.33

RoundingMode

RoundingMode

HALF_UP四舍五入之负数

API文档:向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向上舍入。如果被舍弃部分 >= 0.5,则舍入行为同 RoundingMode.UP;否则舍入行为同 RoundingMode.DOWN。注意,此舍入模式就是通常学校里讲的四舍五入。

这很四舍五入,但和前面的Math.round不一样

half_up

7.4底层存储

每一位数字,都是byte类型数组中的一位,负数存-的ASCII码,小数点.也是

数组最大长度21亿多,但基本碰不到极限。

底层

bigdecimal存储原理

7.5 实现Random随机产生小数点后两位的小数

利用BigDecimal的setScale方法

Random r = new Random();
double one = r.nextDouble(100);
System.out.println(one);  // 56.99038148560035

BigDecimal bd =  BigDecimal.valueOf(one);
System.out.println(bd);  // 56.99038148560035

double result = bd.setScale(2, RoundingMode.HALF_UP).doubleValue();
System.out.println(result);  // 56.99

8. String.compareTo()

System.out.println("Abc".compareTo("Bzz"));  // -1  (A-B)
System.out.println("Abc".compareTo("azz"));  // -32 (A-a)
System.out.println("Cbc".compareTo("Azz"));  // 2  (C-A)
System.out.println("1bc".compareTo("1cz"));   // -1 (b-c)
System.out.println("1bc".compareTo("acz"));   // -48 (49-97)

// 位数不一样
System.out.println("Abc".compareTo("AbcA"));  // -1  (没有 - 有)
System.out.println("AbcA".compareTo("Abc"));  // 1  (有 - 没有)
System.out.println("AbcAAAA".compareTo("Abc"));  // 4  (多4位 - 没有)
System.out.println("Abc".compareTo("AbcBBBBB"));  // -5  (没有 - 多5位)

9. Double.compare

public static int compare(double d1, double d2)

比较两个指定的 double 值。返回整数值:

如果 d1 == d2,则返回 0;

如果 d1 < d2,则返回小于 0 的值(-1);

如果 d1 > d2,则返回大于 0 的值(1)。

int compare = Double.compare(287.6, 287.6);
System.out.println(compare);  // 0

System.out.println(Double.compare(-22, -12));   // -1
System.out.println(Double.compare(-22.1, -12.2));  // -1
System.out.println(Double.compare(29.99, 20.333));   // 1

day19正则表达式

1.1 正则表达式——字符类

  • []里面的内容出现一次。
  • ()分组
  • 语法示例:
  1. [abc]:代表a或者b,或者c字符中的一个
  2. [^abc]:代表除a,b,c以外的任何字符。
  3. [a-z]:代表a-z的所有小写字符中的一个
  4. [A-Z]:代表A-Z的所有大写字符中的一个
  5. [0-9]:代表0-9之间的某一个数字字符。
  6. [a-zA-Z0-9]:代表a-z或者A-Z或者0-9之间的任意一个字符。 (包括头尾的范围)
  7. [a-dm-p]:a 到 d 或 m 到 p之间的任意一个字符。也可以写成"[a-z[m-p]]"但不推荐,Redundant nested character class冗余嵌套字符类。

String.matches()默认从头比倒尾

System.out.println("a".matches("[abc]"));   // true
System.out.println("aa".matches("[abc]"));  // false
System.out.println("z".matches("[abc]"));   // false

System.out.println("z".matches("[^abc]"));   // true
System.out.println("a".matches("[^abc]"));   // false
System.out.println("zz".matches("[^abc][^abc]"));   // true

System.out.println("x".matches("[a-zA-Z]"));   // true
System.out.println("Zs".matches("[a-zA-Z][a-zA-Z]"));   // true

1.2 正则表达式-逻辑运算符

  • 语法示例:
    1. &&:并且
    2. | :或者
    3. \ :转义字符
  • 有逻辑表达式,但不可听信IDEA'[a-p&&def]' can be simplified to 'a-p&&def'省略外围的[],否则有异常情况↓
// JDK17下的结果:
// 必须是数字和字符 必须是4位
System.out.println("23dF".matches("[\\w&&[^_]]{4}"));  // 有括号,true
System.out.println("23dF".matches("[a-zA-Z0-9]{4}"));   //逻辑上和上面一样,true

System.out.println("23dF".matches("\\w&&[^_]{4}"));    // 没括号,&&被当做字符串,false
System.out.println("w&&aaaa".matches("\\w&&[^_]{4}"));  // JDK17 true
System.out.println("w&&aa你a".matches("\\w&&[^_]{4}"));  // true
System.out.println("w&&aa_a".matches("\\w&&[^_]{4}"));  // false
System.out.println("w&&aaaaa".matches("\\w&&[^_]{4}"));  // false

23.03.17拷打chatGPT和Bing chat AI

如果在正则表达式中使用&&而不是将它放在字符集合[]中,则其仅仅表示字符串&&,不再具有逻辑与的运算符的含义。

在正则表达式中,[]用于表示一个字符集合,表示匹配其中的任意一个字符;而&&是逻辑与运算符,在字符集合中使用时可以用来表示多个条件的交集。

例如,正则表达式[abc&&def]表示匹配字符"a"、"b"、"c"和"d"、"e"、"f"中的任意一个。而正则表达式[\w&&[^_]]表示匹配任意一个字母、数字或下划线,但不包括下划线。

因此,如果将&&放在[]外面,它就失去了逻辑运算符的作用,只是简单的匹配字符串中的&&字符。实例代码见下面1.4

网页版ChatGPT修正,JDK版本不同,对结果具有不同影响,JDK11引入了新引擎,因此&&不在[]外会失去逻辑运算符的功能,而JDK11以前,可能并不会失去,继续作为逻辑运算符。

需要注意的是,这个代码的运行结果在不同的 Java 版本中可能会不一样,因此在写代码时最好还是避免使用特殊的符号,或者在写正则表达式时显式地使用括号或转义字符来明确符号的含义。

  • 没有使用方括号来定义字符集,所以每个字符都被当作字面量。

answer from Bing Chat AI:

// 这个正则表达式只能匹配字符 ‘d’、‘e’ 或者 ‘f’。而字符串 “f” 匹配了其中一个字符,所以结果是 true。
System.out.println("f".matches("[a-p&&def]"));   // true

// 正则表达式 “a-p&&def” 表示一个序列,匹配字符串 “a-p&&def”。这是因为没有使用方括号来定义字符集,所以每个字符都被当作字面量。
// 因此,这个正则表达式只能匹配字符串 “a-p&&def”。而字符串 “f” 不匹配,所以结果是 false。
System.out.println("f".matches("a-p&&def"));   // false
  • [a-p&&def] : a-p和def的交集。为:d,e,f。也可以写成[a-p&&[def]]但不推荐,Redundant nested character class冗余嵌套字符类。
// [a-p&&[def]] a-p和def的交集。为:d,e,f
System.out.println("f".matches("[a-p&&[def]]"));  // true 不推荐Redundant nested character class 

System.out.println("f".matches("[a-p&&def]"));   // true
System.out.println("b".matches("[a-p&&def]"));   // false
System.out.println("d".matches("[a-p&&def]"));    // true
  • [a-z&&[^bc]] : a-z和非bc的交集。(等同于[ad-z])
System.out.println("a".matches("[a-z&&[^bc]]"));  // true
System.out.println("b".matches("[a-z&&[^bc]]"));  // false
System.out.println("c".matches("[a-z&&[^bc]]"));  // false
System.out.println("d".matches("[a-z&&[^bc]]"));  // true
System.out.println("e".matches("[a-z&&[^bc]]"));  // true
  • [a-z&&[^m-p]] a到z和除了m到p的交集。(等同于[a-lq-z])
System.out.println("a".matches("[a-z&&[^m-p]]"));  // true
System.out.println("m".matches("[a-z&&[^m-p]]"));  // false
System.out.println("o".matches("[a-z&&[^m-p]]"));  // false
System.out.println("z".matches("[a-z&&[^m-p]]"));  // true

代码示例:

String str = "had";
//1.要求字符串是小写辅音字符开头,后跟ad
String regex = "[a-z&&[^aeiou]]ad";
System.out.println("1." + str.matches(regex));

//2.要求字符串是aeiou中的某个字符开头,后跟ad
regex = "[a|e|i|o|u]ad";//这种写法相当于:regex = "[aeiou]ad";
System.out.println("2." + str.matches(regex));
  • \要转义,/不用
// \ 转义字符 改变后面那个字符原本的含义
//练习:以字符串的形式打印一个双引号
//"在Java中表示字符串的开头或者结尾

//此时\表示转义字符,改变了后面那个双引号原本的含义
//把他变成了一个普普通通的双引号而已。
System.out.println("\"");

// \表示转义字符
//两个\的理解方式:前面的\是一个转义字符,改变了后面\原本的含义,把他变成一个普普通通的\而已。
System.out.println("c:Users\\george\\IdeaProjects\\RegexDemo1.java");

1.3 正则表达式-预定义字符

只匹配一个

  • 语法示例:
    1. "." : 匹配任何字符。

    下面的,实际运用时往往是“\\”,两根反斜杠,一根用于转义。就不用加[]了。

    1. "\d":任何数字[0-9]的简写;(0-9),不包括11,12这些两位数及以上。
    2. "\D":任何非数字[^0-9]的简写;
    3. "\s": 空白字符:[ \t\n\x0B\f\r] 的简写
    4. "\S": 非空白字符:[^\s] 的简写
    5. "\w":单词字符:[a-zA-Z_0-9]的简写,别漏了:_
    6. "\W":非单词字符:[^\w]
  • 代码示例:
//.表示任意一个字符
System.out.println("你".matches("..")); //false
System.out.println("你".matches(".")); //true
System.out.println("你a".matches(".."));//true

// \\d 表示任意的一个数字
// \\d只能是任意的一位数字
// 简单来记:两个\表示一个\
System.out.println("a".matches("\\d")); // false
System.out.println("3".matches("\\d")); // true
System.out.println("33".matches("\\d")); // false
System.out.println("33".matches("\\d\\d")); // true

//\\w只能是一位单词字符[a-zA-Z_0-9]
System.out.println("z".matches("\\w"));   // true
System.out.println("2".matches("\\w"));   // true
System.out.println("21".matches("\\w")); // false
System.out.println("你".matches("\\w"));  //false
System.out.println("_".matches("\\w"));  //true

// 非单词字符
System.out.println("你".matches("\\W")); // true
System.out.println("*".matches("\\W")); // true
System.out.println("(".matches("\\W")); // true
System.out.println(".".matches("\\W")); // true

1.4 正则表达式-数量词

  • 语法示例:
    1. X? : 0次或1次
    2. X* : 0次到多次
    3. X+ : 1次或多次
    4. X{n} : 恰好n次
    5. X{n,} : 至少n次
    6. X{n,m}: n到m次(n和m都是包含的)逗号后面不能加空格,否则会报错 java.util.regex.PatternSyntaxException
// 必须是数字 字母 下划线 至少 6位
System.out.println("zbd_Z10".matches("\\w{6,}"));  // true
System.out.println("zbd_Z你0".matches("\\w{6,}"));   // false
System.out.println("zd_Z0".matches("\\w{6,}"));   // false

// 必须是数字和字符 必须是4位
System.out.println("23dF".matches("[a-zA-Z0-9]{4}"));//true
System.out.println("23 F".matches("[a-zA-Z0-9]{4}"));//false
System.out.println("23dF".matches("[\\w&&[^_]]{4}"));//true
System.out.println("23_F".matches("[\\w&&[^_]]{4}"));//false

1.45 忽略大小写的写法 (?i)

//忽略大小写的书写方式
//在匹配的时候忽略abc的大小写
String regex1 = "(?i)abc";
System.out.println("abc".matches(regex1));  // true
System.out.println("ABC".matches(regex1));  // true
System.out.println("aBC".matches(regex1));  // true

// 不忽略a,只忽略bc
String regex2 = "a(?i)bc";
System.out.println("abc".matches(regex2));  // true
System.out.println("ABC".matches(regex2));  // false
System.out.println("aBC".matches(regex2));  // true

// 只忽略b
String regex4 = "a((?i)b)c";
System.out.println("abc".matches(regex4));//true
System.out.println("ABC".matches(regex4));//false
System.out.println("aBc".matches(regex4));//true

1.5 常见正则表达式

IDEA插件any-rule快速生成

以下来源ChatGPT API调用模式回答:

1.5.1 邮箱

全球邮箱的Java正则表达式:

String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+[a-zA-Z]{2,}$";

解释:

  • ^ 表示匹配字符串的开始位置。(匹配开头?)
  • [a-zA-Z0-9._%+-]+表示匹配邮箱的用户名部分,其中:
    • [a-zA-Z0-9] 表示用户名可以由字母和数字组成。
    • ._%+- 表示用户名可以包含特定字符(. _ % + -)。
    • + 表示用户名至少包含一个字符。
  • @ 表示匹配邮箱中的@符号。
  • [a-zA-Z0-9.-]+表示匹配邮箱的域名部分,其中:
    • [a-zA-Z0-9] 表示域名可以由字母和数字组成。
    • .- 表示域名可以包含特定字符(. -)。**注意这里的.它可以匹配域名的.,也就是说george.33mail将归于一个字符小串在这里匹配。直到碰到下面的\\.,表示邮箱最后面的那一.
    • + 表示域名至少包含一个字符。
  • \\. 表示匹配邮箱中的.符号,需要使用转义字符 \
  • [a-zA-Z]{2,}表示匹配邮箱的顶级域名部分,其中:
    • [a-zA-Z] 表示顶级域名可以由字母组成。
    • {2,} 表示顶级域名至少包含两个字符。
  • $ 表示匹配字符串的结束位置。(匹配尾巴?)
String a = "3131sasfasd".replaceAll("\\d{2}", "Z");
String b = "3131sasfasd".replaceAll("^\\d{2}", "Z");//仅替换字符串开头的两个数字
String c = "3131sdasfasd".replaceAll("sd", "Z");
String d = "3131sdsfasd".replaceAll("sd$", "Z");//仅替换字符串开头的两个数字
System.out.println(a);//ZZsasfasd
System.out.println(b);//Z31sasfasd
System.out.println(c);//3131ZasfaZ
System.out.println(d);//3131sdsfaZ

但是:

String str6 = "aa3131sasfasd";
String str7 = "31aa3131sasfasd31";
String str8 = "31";
System.out.println(str6.matches("\\d{2}")); //false
System.out.println(str7.matches("^\\d{2}"));//false
System.out.println(str7.matches("\\d{2}$"));//false
System.out.println(str8.matches("^\\d{2}$"));//true

注:该正则表达式无法判断邮箱的真实有效性,只能判断邮箱格式是否正确。

String emailRegex1 = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
// 以下结果都是true
System.out.println("george@nihao.33mail.com".matches(emailRegex1));
System.out.println("george@student.gdut.edu.cn".matches(emailRegex1));
System.out.println("george@qq.cn".matches(emailRegex1));
System.out.println("123456879@163.com".matches(emailRegex1));
System.out.println("123456879@xyz.com.cn".matches(emailRegex1));

黑马思路(微改):

//思路:

  • 在书写邮箱号码正则的时候需要把正确的数据分为三部分
    • 第一部分:@的左边 \\w+
      任意的字母数字下划线,至少出现一次就可以了
    • 第二部分:@ 只能出现一次
    • 第三部分:
    • 1 MARKDOWN_HASH5058f1af8388633f609cadb75a75dc9dMARKDOWNHASH的左边[\\w&&[^]]{2,8}任意的字母加数字,总共出现2-8次(此时不能出现下划线)
    • 2 . → \\.
    • 3 .的右边大写字母,小写字母都可以,只能出现2-3(一次以上)次[a-zA-Z]{2,3}
    • 我们可以把3.2和3.3看成一组,这一组可以出现1次至3次
String regex3 = "\\w+@[\\w&&[^_]]{2,8}(\\.[a-zA-Z0-9]+){1,3}";
// 以下都为true
System.out.println("george@nihao.33mail.com".matches(regex3));
System.out.println("george@student.gdut.edu.cn".matches(regex3));
System.out.println("george@qq.cn".matches(regex3));
System.out.println("123456879@163.com".matches(regex3));
System.out.println("123456879@xyz.com.cn".matches(regex3));

1.5.2 手机

String mobilePhoneRegex = "^1([3-9])\\d{9}$";

解释:

  • ^ 表示匹配字符串的开始位置。
  • (1[3-9]\d{9})表示匹配手机号码的正则表达式,其中:
    • 1 表示手机号的第一位必须是数字 1。
    • [3-9] 表示手机号的第二位可以是数字 3 到 9 中的任意一位。
    • \\d{9} 表示手机号的后面 9 位必须是数字。
  • $ 表示匹配字符串的结束位置。

注:Java中需要使用\\转义\


1.5.3 座机

// 区号以0开头,共3-4位,中间-可省略
String telRegex = "^0\\d{2,3}-?\\d{7,8}$";
// 号码主体不以0开头,长度7-8位
String telRegex = "^0\\d{2,3}-?[1-9]\\d{6,7}$";
String telRegex = "0\\d{2,3}-?[1-9]\\d{6,7}";

System.out.println("020-2324242".matches(telRegex));
System.out.println("02122442".matches(telRegex));
System.out.println("027-42424".matches(telRegex));
System.out.println("0712-3242434".matches(telRegex));

1.5.4 身份证

Bing Chat AI :这是一个匹配中华人民共和国大陆地区居民身份证号的Java正则表达式:

String idRegex = "(\\d{6})(19|20)\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]";

它能够匹配18位身份证号码,其中前6位为地址码,接下来8位为出生日期码,最后4位为顺序码和校验码。

黑马思路:

//410801 1993 02 28 457x
//前面6位:省份,市区,派出所等信息,第一位不能是0,后面5位是任意数字[1-9]\\d{5}
//年的前半段: 18 19 20                                                (18|19|20)
//年的后半段: 任意数字出现两次    \\d{2}
//月份: 01~ 09 10 11 12     (0[1-9]|1[0-2])
//日期: 01~09 10~19 20~29 30 31     (0[1-9]|[12]\\d|3[01])
//后面四位: 任意数字出现3次 最后一位可以是数字也可以是大写x或者小写x \\d{3}[\\dXx]
String regex6 = "[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(@[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]";

System.out.println("41080119930228457x".matches(regex6));
System.out.println("510801197609022309".matches(regex6));
System.out.println("15040119810705387X".matches(regex6));
System.out.println("130133197204039024".matches(regex6));
System.out.println("430102197606046442".matches(regex6));
String regex6 = "[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]";
// 以下都是true
System.out.println("41080119930228457x".matches(regex6));
System.out.println("510801197609022309".matches(regex6));
System.out.println("15040119810705387X".matches(regex6));
System.out.println("130133197204039024".matches(regex6));
System.out.println("430102197606046442".matches(regex6));

1.6 本地数据爬取

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MatcherDemo {
    public static void main(String[] args) {
        String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11," +
                "因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台";
        // 从上面文字中提出所有 JavaXX

        //1.获取正则表达式的对象
        Pattern p = Pattern.compile("Java\\d{0,2}");
        //2.获取文本匹配器的对象
        //拿着m去读取str,找符合p规则的子串
        Matcher m = p.matcher(str);

        //3.利用循环获取
        while (m.find()) {
            String s = m.group();
            System.out.println(s);   
            // 结果:Java  Java8  Java11  Java17  Java17
        }

    }
}

底层原理:

private static void method1(String str) {
    //Pattern:表示正则表达式
    //Matcher: 文本匹配器,作用按照正则表达式的规则去读取字符串,从头开始读取。
    //          在大串中去找符合匹配规则的子串。

    //获取正则表达式的对象
    Pattern p = Pattern.compile("Java\\d{0,2}");
    //获取文本匹配器的对象
    //m:文本匹配器的对象
    //str:大串
    //p:规则
    //m要在str中找符合p规则的小串
    Matcher m = p.matcher(str);

    //拿着文本匹配器从头开始读取,寻找是否有满足规则的子串
    //如果没有,方法返回false
    //如果有,返回true。在底层记录子串的起始索引和结束索引+1
    // 0,4
    boolean b = m.find();

    //方法底层会根据find方法记录的索引进行字符串的截取
    // substring(起始索引,结束索引);包头不包尾
    // (0,4)但是不包含4索引
    // 会把截取的小串进行返回。
    String s1 = m.group();
    System.out.println(s1);

    //第二次在调用find的时候,会继续读取后面的内容
    //读取到第二个满足要求的子串,方法会继续返回true
    //并把第二个子串的起始索引和结束索引+1,进行记录
    b = m.find();

    //第二次调用group方法的时候,会根据find方法记录的索引再次截取子串
    String s2 = m.group();
    System.out.println(s2);
}

1.6.1 多段内容爬取

假如给定一段文本内容,要求爬取里面的邮箱,手机号,座机,热线电话,可以不用定义方法或重复写上面的内容,只用将正则表达式用|或包起来写一起就行,like:

String regex = "(1[3-9]\\d{9})|(\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2})" +
                "|(0\\d{2,3}-?[1-9]\\d{4,9})" +
                "|(400-?[1-9]\\d{2}-?[1-9]\\d{3})";

1.7网络爬取数据

爬取https://m.game234.com/ztwz/20190620/6757745.html所有的身份证号码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexDemo3 {
    public static void main(String[] args) throws IOException {
        /* 扩展需求2:
            把连接:https://m.game234.com/ztwz/20190620/6757745.html
            中所有的身份证号码都爬取出来。
        */

        //创建一个URL对象
        URL url = new URL("https://m.game234.com/ztwz/20190620/6757745.html");
        //连接上这个网址
        //细节:保证网络是畅通
        URLConnection conn = url.openConnection();//创建一个对象去读取网络中的数据
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line;
        //获取正则表达式的对象pattern
        String regex = "[1-9]\\d{17}";
        Pattern pattern = Pattern.compile(regex);//在读取的时候每次读一整行
        while ((line = br.readLine()) != null) {
            //拿着文本匹配器的对象matcher按照pattern的规则去读取当前的这一行信息
            Matcher matcher = pattern.matcher(line);
            while (matcher.find()) {
                System.out.println(matcher.group());
            }
        }
        br.close();
    }
}

1.8 带条件的数据爬取

需求:有如下文本,按要求爬取数据。

Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台。

  • 需求1:爬取版本号为8,11.17的Java文本,但是只要Java,不显示版本号。
  • 需求2:爬取版本号为8,11,17的Java文本。正确爬取结果为:ava8 Java11 Java17 Java17
  • 需求3:爬取除了版本号为8,11,17的Java文本。
public static void main(String[] args) {
    String s = "java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和JAva11," +
            "因为这两个是长期支持版本,下一个长期支持版本是JAVa17,相信在未来不久JAVA17也会逐渐登上历史舞台";

    //?理解为前面的数据Java
    //=表示在Java后面要跟随的数据
    //但是在获取的时候,只获取前半部分
    // : 要同时获取:后面的内容, !不获取
    // String regex = "Java(?=8|11|17)";   // 需求 1
    // String regex = "((?i)Java)(?=8|11|17)";   // 需求 1  增加忽略Java大小写
    // String regex = "((?i)Java)(8|11|17)";     // 需求 2
    // String regex = "((?i)Java)(?:8|11|17)";     // 需求 2  与上面一样,
     String regex = "((?i)Java)(?!8|11|17)";     // 需求 3

    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(s);

    while (m.find()) {
        System.out.println(m.group());
        // 需求1 运行结果: Java  JAva  JAVa  JAVA
        // 需求2 运行结果 Java8  JAva11  JAVa17  JAVA17
        // 需求3 运行结果 java
    }
}

1.9 贪婪爬取和非贪婪爬取

Java当中,默认的就是贪婪爬取,众所周知,+表示1个及以上,*表示0个及以上

只写+*表示贪婪匹配,如果在+*后面加问号?表示非贪婪爬取
+? 非贪婪匹配
*? 非贪婪匹配
贪婪爬取:在爬取数据的时候尽可能的多获取数据
非贪婪爬取:在爬取数据的时候尽可能的少获取数据

举例:
如果获取数据:ab+
贪婪爬取获取结果:abbbbbbbbbbbb
非贪婪爬取获取结果:ab

public static void main(String[] args) {
    String s = "Java自从95年问世以来,abbbbbbbbbbbbaaaaaaaaaaaaaaaaaa" +
            "经历了很多版木,目前企业中用的最多的是]ava8和]ava11,因为这两个是长期支持版木。" +
            "下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台";

    // String regex = "ab+";  // abbbbbbbbbbbb
     String regex = "ab+?";  // ab

    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(s);
    while (m.find()) {
        System.out.println(m.group());
    }
}

1.10 String的replaceAll方法中使用正则表达式

  • String类的replaceAll()方法原型:
public String replaceAll(String regex,String newStr)
//参数regex表示一个正则表达式。可以将当前字符串中匹配regex正则表达式的字符串替换为newStr。
  • 代码示例:
// 要求1:把字符串中三个姓名之间的字母替换为vs
String s = "小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠";

//细节:
//方法在底层会创建文本解析器的对象
//然后从头开始去读取字符串中的内容,只要有满足的,那么就用第一个参数去替换。
String vs = s.replaceAll("[\\w&&[^_]]+", "vs");
System.out.println(vs);  // 小诗诗vs小丹丹vs小惠惠

1.11 String的split方法中使用正则表达式

  • String类的split()方法原型:
    public String[] split(String regex)
    //参数regex表示正则表达式。可以将当前字符串中匹配regex正则表达式的符号作为"分隔符"来切割字符串。
  • 代码示例:
// 要求2:把字符串中的三个姓名切割出来
String s = "小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠";

//细节:
//方法在底层跟之前一样也会创建文本解析器的对象
//然后从头开始去读取字符串中的内容,只要有满足的,那么就切割。
String[] split = s.split("[\\w&&[^_]]+");  // 匹配的被分隔的内容是 字符,不是文字
for (String s1 : split) {
    System.out.println(s1);   // 小诗诗 \n 小丹丹 \n 小惠惠
}

1.12 扩展

在Java中,有一串字符串“王冰冰, 诸葛亮, 欧阳娜娜, 乔治老话说”,如何将这串字符每个名字中间替换为号,结果:“王*冰, 诸*亮, 欧**娜, 乔**\说”

answer from ChatGPT API调用

  • 芝士点:\p{L}\b
  1. \p{L}

\p{L}是Unicode属性,用于匹配所有的字母字符,包括字母、汉字、日文假名、希腊字母等。在Java中,可以使用\p{L}来匹配所有的汉字,例如:

String pattern = "\\p{L}"; // 匹配所有字母字符,包括汉字
String input = "Hello, 世界!";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(input);
while (m.find()) {
    System.out.println(m.group());
}
// 上述代码中,\p{L}匹配到了汉字"世"和"界",同时也匹配到了字母字符。
// 输出结果: H e l l o 世 界  (分行)
  1. \b

\b是边界匹配符,用于匹配单词边界。单词边界指的是一个单词开始或结束的位置,它不会匹配任何字符,只匹配位置。例如:

String pattern = "\\bworld\\b"; // 匹配单词"world"
String input = "Hello, world!"; 
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(input);
if (m.find()) {
    System.out.println(m.group()); // 输出"world"
}

上述代码中,\b匹配到了"world"两边的单词边界,而不会匹配到"world"中的字母。

需要注意的是,在Java中使用\b时,要将正则表达式的字符串转义一下,变成\\b。因为\b在Java中属于转义字符,用于表示退格符。如果不转义,会导致编译错误。

正题

可以使用正则表达式匹配每个名字,然后使用replace方法将匹配到的部分替换为*号。

String input = "王冰冰, 诸葛亮, 欧阳娜娜, 乔治老话说";
String pattern = "\\b\\p{L}{2}\\p{L}*\\b"; // 匹配至少两个汉字的单词

Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(input);

StringBuffer sb = new StringBuffer();
while (m.find()) {
    String name = m.group();
    // 用chatAt获取名字的第一位,和最后一位,剩下的中间部分被替换
    String maskedName = name.charAt(0) + name.substring(1, name.length()-1).replaceAll(".", "*") + name.charAt(name.length()-1);
    m.appendReplacement(sb, maskedName);
}
m.appendTail(sb);

String output = sb.toString();
System.out.println(output); // 王*冰, 诸*亮, 欧**娜, 乔***说

解释一下代码:

首先定义了一个正则表达式,用于匹配至少两个汉字的单词。这里使用了Unicode属性\p{L}来匹配汉字,以及\b边界匹配符。

然后调用Pattern.compile方法编译正则表达式,然后用Matcher对象去匹配输入字符串。

在匹配过程中,对于每一个匹配到的名字,都将其中间部分替换为*号,然后调用Matcher.appendReplacement方法将替换后的结果加入StringBuffer中。

最后调用Matcher.appendTail方法将剩余的字符串也加入StringBuffer中,得到最终的输出字符串。


1.13 正则表达式-分组括号( )

正则表达式,()为一组,每组是有序号(组号)的,从1开始,连续不间断。以左括号为基准,最左边的是第一组,其次为第二组,以此类推。

正则分组

\\组号:表示把第X组的内容再出来用一次

String regex1 = "(.).+\\1";
System.out.println("a123a".matches(regex1));
System.out.println("b456b".matches(regex1));
System.out.println("17891".matches(regex1));
System.out.println("&abc&".matches(regex1));
System.out.println("a123b".matches(regex1));
System.out.println("==========================");

//需求2:判断一个字符串的开始部分和结束部分是否一致?可以有多个字符
//举例: abc123abc b456b 123789123 &!@abc&!@ abc123abd(false)
String regex2 = "(.+).+\\1";
System.out.println("abc123abc".matches(regex2));
System.out.println("b456b".matches(regex2));
System.out.println("123789123".matches(regex2));
System.out.println("&!@abc&!@".matches(regex2));
System.out.println("abc123abd".matches(regex2));
System.out.println("==========================");

//需求3:判断一个字符串的开始部分和结束部分是否一致?开始部分内部每个字符也需要一致
//举例: aaa123aaa bbb456bbb 111789111 &&abc&&
//(.):把首字母看做一组
// \\2:把首字母拿出来再次使用
// *:作用于\\2,表示后面重复的内容出现0次或多次
String regex3 = "((.)\\2*).+\\1";
System.out.println("aaa123aaa".matches(regex3));
System.out.println("bbb456bbb".matches(regex3));
System.out.println("111789111".matches(regex3));
System.out.println("&&abc&&".matches(regex3));

正则外部使用:$1 表示把正则表达式中第一组的内容,再拿出来用。

例题:需求:

​ 将字符串:我要学学编编编编程程程程程程。

​ 替换为:我要学编程

String str = "我要学学编编编编程程程程程程";

//需求:把重复的内容 替换为 单个的
//学学                学
//编编编编            编
//程程程程程程        程
//  (.)表示把重复内容的第一个字符看做一组
//  \\1表示第一字符再次出现
//  + 至少一次
//  $1 表示把正则表达式中第一组的内容 (.) (第一个字),再拿出来用
String result = str.replaceAll("(.)\\1+", "$1");
System.out.println(result);

1.14 非捕获分组

非捕获分组:分组之后不需要再用本组数据,仅仅是把数据括起来。不占用组号。

String regexx ="[1-9]\\d{16}(?:\\d|x|x)\\1";      // 报错,这里不占用组号

非捕获分组

day20 日期,包装类

1. Date

1.1 概述

java.util.Date类 表示特定的瞬间,精确到毫秒。

构造函数:

  • public Date()

    从运行程序的此时此刻到时间原点经历的毫秒值this(System.currentTimeMillis());,转换成Date对象,分配Date对象并初始化此对象,以表示分配它的时间(精确到毫秒)。

  • public Date(long date)

    将指定参数的毫秒值date,转换成Date对象,分配Date对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即1970年1月1日00:00:00 GMT)以来的指定毫秒数。

中国东八区(GMT+08:00)是比世界协调时间/格林尼治时间(GMT)快8小时的时区,当格林尼治标准时间为0:00时,东八区的标准时间为08:00。

System.out.println(new Date());   // Sun Mar 19 22:26:13 CST 2023
System.out.println(new Date(0L));  // Thu Jan 01 08:00:00 CST 1970

tips:在使用println方法时,会自动调用Date类中的toString方法。Date类对Object类中的toString方法进行了覆盖重写,所以结果为指定格式的字符串。

1.2 常用方法

  • public boolean before(Date when)
  • public boolean after(Date when)
  • public long getTime() 把日期对象转换成对应的时间毫秒值。获取的是日期对象从1970年1月1日 00:00:00到现在的毫秒值
  • public void setTime(long time) 把方法参数给定的毫秒值设置给日期对象,设置时间,给的是毫秒值
Date d = new Date();
System.out.println(d.getTime());   // 1679236340142
System.out.println(d.getTime() * 1.0 / 1000 / 60 / 60 / 24 / 365 + "年");   // 53.24823760533993年
System.out.printf("%.2f年\n", (d.getTime() * 1.0 / 1000 / 60 / 60 / 24 / 365));   // 53.25年
// =====================================================================
// 365 后面 + L,表示long型
long time = 365L * 24 * 60 * 60 * 1000;
d.setTime(time);
System.out.println(d.getTime());  // 31536000000 现在get到的就是上面set的数值
System.out.println(d);   // Fri Jan 01 08:00:00 CST 1971

数值计算不加 L 可能有出错!

Date d = new Date();
d.setTime(d.getTime() + 200L * 24 * 60 * 60 * 1000); // 200天之后  Fri Oct 06 16:18:34 CST 2023
//        d.setTime(d.getTime() + (200 * 24 * 60 * 60 * 1000)); // Tue Mar 21 20:05:29 CST 2023
System.out.println(d);

小结:Date表示特定的时间瞬间,我们可以使用Date对象对时间进行操作。

2. SimpleDateFormat

java.text.SimpleDateFormat继承于抽象父类DateFormat, 是日期/时间格式化类,通过这个类可以帮我们完成日期和文本之间的转换,也就是可以在Date对象与String对象之间进行来回转换。

  • 格式化:按照指定的格式,把Date对象转换为String对象。
  • 解析:按照指定的格式,把String对象转换为Date对象。

2.1 构造方法

  • public SimpleDateFormat(String pattern):用给定的模式和默认语言环境的日期格式符号构造SimpleDateFormat。参数pattern是一个字符串,代表日期时间的自定义格式。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

2.2 格式规则

常用的格式规则为:

标识字母(区分大小写) 含义
y
M
d
H
m
s
E Day name in week (Tuesday; Tue)
u Day number of week (1 = Monday, ..., 7 = Sunday)
a AM/pm

备注:更详细的格式规则,可以参考SimpleDateFormat类的API文档。

亲测,这里MM写成M,HH这些两位写成一位的话,结果将去除用来补足两位数的0

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-M-d H:m:s");

Date d = new Date(new Random().nextInt());  // int范围内取
// 1970-01-01 08:00:00  或  // 1970-01-24 16:36:41
System.out.println(sdf.format(d));
// 1970-1-1 8:0:0  或  // 1970-1-24 16:36:41
System.out.println(sdf1.format(d));

2.3 常用方法

DateFormat类的常用方法有:

  • public String format(Date date):将Date对象格式化为字符串。
  • public Date parse(String source):将字符串解析为Date对象。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String format = sdf.format(new Date());
System.out.println(format);  // 2023-03-19 23:23:42

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

Date d = sdf.parse("2023-04-19 23:55:00");
System.out.println(d);   // Wed Apr 19 23:55:00 CST 2023
System.out.println( (d.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24) + "日");  // 31日

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

SimpleDateFormat ss = new SimpleDateFormat("yyyy年MM月dd日 HH时:mm分:ss秒 EE");
String s = ss.format(new Date());
System.out.println(s);  // 2023年03月19日 23时:34分:41秒 周日

3. Calendar

3.1 概述

  • java.util.Calendar类表示一个“日历类”,可以进行日期运算。它是一个抽象类,不能创建对象,我们可以使用它的子类:java.util.GregorianCalendar类。
  • 有两种方式可以获取GregorianCalendar对象:
    • 直接创建GregorianCalendar对象;
    • 通过Calendar的静态方法getInstance()方法获取GregorianCalendar对象

3.2 常用方法

方法名 说明
public static Calendar getInstance() 获取一个它的子类GregorianCalendar对象。
public int get(int field) 获取某个字段的值。field参数表示获取哪个字段的值,
可以使用Calender中定义的常量来表示:
Calendar.YEAR : 年
Calendar.MONTH :月(0-11)。使用时记得 +1
Calendar.DAY_OF_MONTH:月中的日期
Calendar.HOUR:小时,取值范围0-11
Calendar.HOUR_OF_DAY表示24小时制中的小时数,取值范围0-23
Calendar.MINUTE:分钟
Calendar.SECOND:秒
Calendar.DAY_OF_WEEK:星期。返回值范围:1--7,分别表示:"星期日","星期一","星期二",...,"星期六"
public void set(int field,int value) 设置某个字段的值。加入只设置了日期,那时间按运行时计算。日期设置50,那就跳到下个月。
public void add(int field,int amount) 为某个字段增加/减少指定的值(负数即减少)
public long getTimeInMillis()
public void setTimeInMillis(long millis)
拿到 / 设置时间毫秒值
public final Date getTime() 获取日期Date对象,return new Date(getTimeInMillis());
public final void setTime(Date date) 给日历设置日期对象,setTimeInMillis(date.getTime());

3.3 get()方法

//1.获取一个子类GregorianCalendar对象
Calendar instance = Calendar.getInstance();

/*java.util.GregorianCalendar[time=1679296254470,......,zone=sun.util.calendar.ZoneInfo[id="Asia/Shanghai",
......,YEAR=2023,MONTH=2,WEEK_OF_YEAR=12,WEEK_OF_MONTH=4,DAY_OF_MONTH=20,DAY_OF_YEAR=79,DAY_OF_WEEK=2,
DAY_OF_WEEK_IN_MONTH=3,AM_PM=1,HOUR=3,HOUR_OF_DAY=15,MINUTE=10,SECOND=54,MILLISECOND=470,......*/
System.out.println(instance);  // //2.打印子类对象

int year = instance.get(Calendar.YEAR);  // 2023
int month = instance.get(Calendar.MONTH) + 1;   // 3
int day = instance.get(Calendar.DAY_OF_MONTH);   // 20

int hour = instance.get(Calendar.HOUR);    // 3(下午3点)
int hour1 = instance.get(Calendar.HOUR_OF_DAY);   // 15

int minute = instance.get(Calendar.MINUTE);   // 17
int second = instance.get(Calendar.SECOND);    // 47
int week = instance.get(Calendar.DAY_OF_WEEK);   // 2(周一)

// 2023年3月20日15:47:29
System.out.println(year + "年" + month + "月" + day + "日" + hour1 + ":" + minute + ":" + second);

3.4 set()方法

public static void main(String[] args) {
    Calendar instance = Calendar.getInstance();

    // 计算金川纱耶出生那天星期几
    instance.set(Calendar.YEAR, 2001);
    instance.set(Calendar.MONTH, 10-1);  // 转换为Calendar内部的月份值
    instance.set(Calendar.DAY_OF_MONTH, 31);

    int week = instance.get(Calendar.DAY_OF_WEEK);
    System.out.println("金川纱耶出生那天是:" + getWeek(week)); // 星期三

    System.out.println(instance);  // 没有赋值的字段如时间,按运行时自动设置。

}

public static String getWeek(int week) {
    String[] weekArray = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
    //            索引      [0]      [1]       [2]      [3]       [4]      [5]      [6]
    return weekArray[week - 1];
}

3.5 add()方法

//计算200天以后是哪年哪月哪日,星期几?
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DAY_OF_MONTH, 200);

int year = instance.get(Calendar.YEAR);
int month = instance.get(Calendar.MONTH) + 1;
int day = instance.get(Calendar.DAY_OF_MONTH);
System.out.println(year + "年" + month + "月" + day + "日");  // 2023年10月6日
int week = instance.get(Calendar.DAY_OF_WEEK);  // 星期五
System.out.println(getWeek(week));

4. JDK8时间相关类

JDK8时间类

JDK8all类

JDK8时间类类名 作用
ZoneId 时区
Instant 时间戳
ZoneDateTime 带时区的时间
DateTimeFormatter 用于时间的格式化和解析
LocalDate 年、月、日
LocalTime 时、分、秒
LocalDateTime 年、月、日、时、分、秒
Duration 时间间隔(秒,纳,秒)
Period 时间间隔(年,月,日)
ChronoUnit 时间间隔(所有单位)

4.1 ZoneId 时区

方法名 说明
static Set\<string> getAvailableZoneIds() 获取Java中支持的所有时区
static ZoneId systemDefault() 获取系统默认时区
static Zoneld of(string zoneld) 获取一个指定时区
//1.获取所有的时区名称
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
System.out.println(zoneIds.size());//602
System.out.println(zoneIds);// ...Asia/Shanghai

//2.获取当前系统的默认时区
ZoneId zoneId = ZoneId.systemDefault();
System.out.println(zoneId);//Asia/Shanghai

//3.获取指定的时区
ZoneId zoneId1 = ZoneId.of("Asia/Pontianak");
System.out.println(zoneId1);//Asia/Pontianak

4.2 Instant 时间戳

方法名 说明
static Instant now() 获取当前时间的Instant对象(标准时间)
static Instant ofXxxx(long epochMilli) 根据(秒/毫秒/纳秒)获取Instant对象
ZonedDateTime atZone(ZoneIdzone) 指定时区
boolean isxxx(Instant otherInstant) 判断系列的方法
Instant minusXxx(long millisToSubtract) 减少时间系列的方法
Instant plusXxx(long millisToSubtract) 增加时间系列的方法
//1.获取当前时间的Instant对象(标准时间)
Instant now = Instant.now();
System.out.println(now);  // 2023-03-20T13:51:00.908207500Z

//2.根据(秒/毫秒/纳秒)获取Instant对象  (秒 毫秒 微妙 纳秒)
Instant instant = Instant.ofEpochMilli(0L); // 毫秒
System.out.println(instant);  // 1970-01-01T00:00:00Z

Instant instant1 = Instant.ofEpochSecond(1L);  // 秒钟
System.out.println(instant1);  //  1970-01-01T00:00:01Z

Instant instant2 = Instant.ofEpochSecond(1L, 1000000000L);// 纳秒
System.out.println(instant2);  // 1970-01-01T00:00:02Z  (1s + 1s)

// 3.指定时区
ZonedDateTime zonedDateTime = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime);  // 2023-03-20T22:04:09.882249300+08:00[Asia/Shanghai]

//4.isXxx 判断
Instant instant3 = Instant.ofEpochMilli(0L);
Instant instant4 = Instant.ofEpochMilli(1000L);
//5.用于时间的判断
System.out.println(instant3.isBefore(instant4));  // true //isBefore:判断调用者代表的时间是否在参数表示时间的前面
System.out.println(instant3.isAfter(instant4));  // false //isAfter:判断调用者代表的时间是否在参数表示时间的后面

//6.Instant minusXxx(long millisToSubtract) 减少时间系列的方法
Instant instant5 = Instant.ofEpochMilli(0L);
Instant instant6 = instant5.minusMillis(1000L);
System.out.println(instant6);  // 1969-12-31T23:59:59Z
//7. Instant plusXxx(long millisToSubtract)
Instant instant7 = instant5.plusMillis(1000L);
System.out.println(instant7);  // 1970-01-01T00:00:01Z

4.3 ZoneDateTime 带时区的时间

方法名 说明
static ZonedDateTime now() 获取当前时间的ZonedDateTime对象
static ZonedDateTime ofXxxx(...) 获取指定时间的ZonedDateTime对象
ZonedDateTime withXxx(时间) 修改时间系列的方法
ZonedDateTime minusXxx(时间) 减少时间系列的方法
ZonedDateTime plusXxx(时间) 增加时间系列的方法
// //1.获取当前时间对象(带时区)
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now);  // 2023-03-20T22:25:47.544883600+08:00[Asia/Shanghai]
//2.获取指定的时间对象(带时区)  年月日时分秒纳秒方式指定
ZonedDateTime time = ZonedDateTime.of(2023, 10, 1, 11, 13, 12, 0, ZoneId.of("Asia/Shanghai"));
System.out.println(time); // 2023-10-01T11:13:12+08:00[Asia/Shanghai]

//通过Instant + 时区的方式指定获取时间对象
Instant instant = Instant.ofEpochMilli(0L);
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, zoneId);
System.out.println(zonedDateTime);  // 1970-01-01T08:00+08:00[Asia/Shanghai]

//3.withXxx 修改时间系列的方法
ZonedDateTime withYear = zonedDateTime.withYear(2000);
System.out.println(withYear);  // 2000-01-01T08:00+08:00[Asia/Shanghai]

//4. 减少时间
ZonedDateTime z1 = withYear.minusYears(1);
System.out.println(z1);  // 1999-01-01T08:00+08:00[Asia/Shanghai]
//5.增加时间
ZonedDateTime z2 = z1.plusDays(20);
System.out.println(z2);  // 1999-01-21T08:00+08:00[Asia/Shanghai]

JDK8新增的时间对象都是不可变的

如果我们修改了,减少了,增加了时间

那么调用者是不会发生改变的,会产生一个新的时间。

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

方法名 说明
static DateTimeFormatter ofPattern(String 格式) 获取格式对象
String format(时间对象) 按照指定方式格式化
// 获取时间对象
ZonedDateTime time = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
// // 解析/格式化器
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EE a");
// 格式化
System.out.println(dtf.format(time));  // 2023-03-20 22:49:38 周一 下午

4.5LocalDate 年、月、日

时间类

转换

//1.获取当前时间的日历对象(包含 年月日)
LocalDate now = LocalDate.now();
System.out.println(now);  // 2023-03-20

// 2.获取指定的时间的日历对象
LocalDate ldate = LocalDate.of(2023, 6, 12);
System.out.println(ldate);  // 2023-06-12
// 3. get
System.out.println(ldate.getYear());   // 年 2023
System.out.println(ldate.getMonthValue());  // 月份数值 6
// Month对象
Month month = ldate.getMonth();
System.out.println(month);  // JUNE
System.out.println(month.getValue());  // 6

System.out.println(ldate.getDayOfMonth());  // 日 12
System.out.println(ldate.getDayOfYear());  // 一年中第几天 163
System.out.println(ldate.getDayOfWeek());  // 星期  MONDAY
System.out.println(ldate.getDayOfWeek().getValue());  // 星期  1
//is开头的方法表示判断
System.out.println(now.isBefore(ldate));  // true
System.out.println(now.isAfter(ldate));   // false
//with开头的方法表示修改,只能修改年月日
LocalDate localDate = now.withYear(2055);  // 2055-03-20
System.out.println(localDate);
//minus开头的方法表示减少,只能减少年月日
LocalDate localDate1 = now.minusYears(30);  // 1993-03-20
System.out.println(localDate1);
//plus开头的方法表示增加,只能增加年月日
LocalDate localDate2 = now.plusDays(11);  // 2023-03-31
System.out.println(localDate2);

判断今天是否是你的生日,MonthDay.of

// 判断今天是否是你的生日
LocalDate birDate = LocalDate.of(2000, 1, 1);
LocalDate nowDate1 = LocalDate.now();

MonthDay birMd = MonthDay.of(birDate.getMonthValue(), birDate.getDayOfMonth());
MonthDay nowMd = MonthDay.from(nowDate1);

System.out.println("今天是你的生日吗? " + birMd.equals(nowMd));//今天是你的生日吗?

4.6 LocalTime 时、分、秒

LocalTime now = LocalTime.now();
System.out.println(now);  // 23:45:18.506838700

System.out.println(now.getHour());  // 23
System.out.println(now.getMinute());  // 45
System.out.println(now.getSecond());  // 18
System.out.println(now.getNano());  // 506838700
System.out.println("------------------------------------");
System.out.println(LocalTime.of(8, 20));//时分  08:20
System.out.println(LocalTime.of(8, 20, 30));//时分秒  08:20:30
System.out.println(LocalTime.of(8, 20, 30, 150));//时分秒纳秒 08:20:30.000000150

// isBefoe isAfter
// minusXXX plusXXX
// withXXX //with系列的方法,只能修改时、分、秒
LocalTime localTime = now.withHour(10);
System.out.println(localTime);  // 10:48:43.953427

4.7 LocalDateTime 年、月、日、时、分、秒

// 当前时间的的日历对象(包含年月日时分秒)
LocalDateTime now = LocalDateTime.now();
System.out.println(now);  // 2023-03-20T23:51:28.356352700
// get... 基本就是上面LocalTime LocalDate的交集

// 转换
LocalDate localDate = now.toLocalDate();
System.out.println(localDate);  // 2023-03-20

LocalTime localTime = now.toLocalTime();
System.out.println(localTime);  // 23:55:26.107813200

4.8 Duration 时间间隔(秒,纳秒)

// Duration.between(past, now),第二个参数减第一个参数
public static Duration between(Temporal startInclusive, Temporal endExclusive)

//duration.toXXX()
Duration duration = Duration.between(birDate, now);  // //第二个参数减第一个参数
System.out.println("相差的时间间隔对象:" + duration);  // 相差的时间间隔对象:PT187456H52M27.393328S  (注意中间 H小时 M分钟)
System.out.println("============================================");
System.out.println(duration.toDays());//两个时间差的天数  // 7810
System.out.println(duration.toHours());//两个时间差的小时数  // 187456(和上面数据一致)
System.out.println(duration.toMinutes());//两个时间差的分钟数  // 11247412
System.out.println(duration.toMillis());//两个时间差的毫秒数   // 674844747393
System.out.println(duration.toNanos());//两个时间差的纳秒数  // 674844747393328000
System.out.println(duration.toHoursPart());  // 上面的(toHours() % 24);
System.out.println(duration.toMinutesPart()); // 和第一行字符串(52M数据一致)

4.9 Period 时间间隔(年,月,日)

//第二个参数减第一个参数
public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive)

period.getXXX();  
// 当前本地 年月日
LocalDate today = LocalDate.now();
// 生日的 年月日
LocalDate birthDate = LocalDate.of(2001, 10, 31);

//第二个参数减第一个参数
Period period = Period.between(birthDate, today);  

System.out.println("相差的时间间隔对象:" + period);  // 相差的时间间隔对象:P21Y4M21D
System.out.println(period.getYears());  // 21 
System.out.println(period.getMonths());  // 4
System.out.println(period.getDays());  // 21

System.out.println(period.toTotalMonths());  // 总共多少月,256

4.10 ChronoUnit 时间间隔(所有单位)

// 当前时间
LocalDateTime today = LocalDateTime.now();
System.out.println(today);
// 生日时间
LocalDateTime birthDate = LocalDateTime.of(2000, 1, 1,0, 0, 0);
System.out.println(birthDate);

System.out.println("相差的年数:" + ChronoUnit.YEARS.between(birthDate, today));
System.out.println("相差的月数:" + ChronoUnit.MONTHS.between(birthDate, today));
System.out.println("相差的周数:" + ChronoUnit.WEEKS.between(birthDate, today));
System.out.println("相差的天数:" + ChronoUnit.DAYS.between(birthDate, today));
System.out.println("相差的时数:" + ChronoUnit.HOURS.between(birthDate, today));
System.out.println("相差的分数:" + ChronoUnit.MINUTES.between(birthDate, today));
System.out.println("相差的秒数:" + ChronoUnit.SECONDS.between(birthDate, today));
System.out.println("相差的毫秒数:" + ChronoUnit.MILLIS.between(birthDate, today));
System.out.println("相差的微秒数:" + ChronoUnit.MICROS.between(birthDate, today));
System.out.println("相差的纳秒数:" + ChronoUnit.NANOS.between(birthDate, today));
System.out.println("相差的半天数:" + ChronoUnit.HALF_DAYS.between(birthDate, today));
System.out.println("相差的十年数:" + ChronoUnit.DECADES.between(birthDate, today));
System.out.println("相差的世纪(百年)数:" + ChronoUnit.CENTURIES.between(birthDate, today));
System.out.println("相差的千年数:" + ChronoUnit.MILLENNIA.between(birthDate, today));
System.out.println("相差的纪元数:" + ChronoUnit.ERAS.between(birthDate, today));

// 2023-03-21T00:25:48.423131700
2000-01-01T00:00
相差的年数:23
相差的月数:278
相差的周数:1211
相差的天数:8480
相差的时数:203520
相差的分数:12211225
相差的秒数:732673548
相差的毫秒数:732673548423
相差的微秒数:732673548423131
相差的纳秒数:732673548423131700
相差的半天数:16960
相差的十年数:2
相差的世纪(百年)数:0
相差的千年数:0
相差的纪元数:0

5. 包装类

5.1 概述

Java提供了两个类型系统,基本类型引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:

基本类型 对应的包装类(位于java.lang包中)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

包装类

  • 方法形参如果是Object,那么基本数据类型就存不了。
  • 集合存的也只能是引用数据类型。

5.2 Integer类

  • Integer类构造方法及静态方法
方法名 说明
public Integer(int value) 根据 int 值创建 Integer 对象(过时)
public Integer(String s) 根据 String 值创建 Integer 对象(过时)
public static Integer valueOf(int i) 返回表示指定的 int 值的 Integer 实例
public static Integer valueOf(String s) 返回保存指定String值的 Integer 对象
public static Integer valueOf(String s, int radix) 根据传递的字符串和进制创建Integer对象
static String tobinarystring(int i) 得到二进制
static String tooctalstring(int i) 得到八进制
static String toHexstring(int i) 得到十六进制
static int parseInt(string s) 将字符串类型的整数转成int类型的整数
// 1.利用构造方法,过时,爆红线(JDK5以前方式)
Integer i1 = new Integer(1);   // 1
Integer i2 = new Integer("1");   // 1
// Integer i3 = new Integer("abc");  // NumberFormatException
// 2.利用静态方法获取 (JDK5以前方式)
Integer integer = Integer.valueOf(101);  // 101
Integer integer1 = Integer.valueOf("1000");   // 1000
Integer integer2 = Integer.valueOf("123", 8);   // 83

转换为进制String:

//1.把整数转成二进制,十六进制
String s = Integer.toBinaryString(100);
System.out.println(s);  // 1100100

//2.把整数转成八进制
String s1 = Integer.toOctalString(100);
System.out.println(s1);   // 144

//3.把整数转成十六进制
String s2 = Integer.toHexString(100);
System.out.println(s2);   // 64

5.2.1 Integer底层原理

建议:获取Integer对象的时候不要自己new,而是采取直接赋值或者静态方法valueOf的方式

因为在实际开发中,-128~127之间的数据,用的比较多。如果每次使用都是new对象,那么太浪费内存了。

所以,Integer类提前把这个范围之内的每一个数据都创建好对象,如果要用到了不会创建新的,而是返回已经创建好的对象。

// 两种方法获取对象的区别
// 实际开发中,-128 - 127之间的数据使用的比较多,
// 如果每次都是new,就太浪费内存了,所以提前把这个范围内每一个数据都创建好对象
// 如果要用到了不会创建新的,而是返回已经创建好的对象。
Integer ii1 = Integer.valueOf(127);
Integer ii2 = Integer.valueOf(127);
System.out.println(ii1 == ii2);      // true

Integer ii3 = Integer.valueOf(128);
Integer ii4 = Integer.valueOf(128);
System.out.println(ii3 == ii4);       // false

Integer is1 = Integer.valueOf("100");
Integer is2 = Integer.valueOf("100");
System.out.println(is1 ==is2);        // true

// new 出来的 地址值肯定不一样
Integer ii5 = new Integer(127);
Integer ii6 = new Integer(127);
System.out.println(ii5 == ii6);     // false

Integer ii7 = new Integer(128);
Integer ii8 = new Integer(128);
System.out.println(ii7 == ii8);     // false

5.3 装箱与拆箱

基本类型与对应的包装类对象之间,来回转换的过程称为”装箱“与”拆箱“:

  • 装箱:从基本类型转换为对应的包装类对象。
  • 拆箱:从包装类对象转换为对应的基本类型。

基本数值---->包装对象

Integer i = new Integer(4);//使用构造函数函数
Integer iii = Integer.valueOf(4);//使用包装类中的valueOf方法 Unnecessary boxing 

包装对象---->基本数值

int num = i.intValue();

5.4 自动装箱与自动拆箱

  • 自动装箱:把基本数据类型自动的变成其对应的包装类。
  • 自动拆箱:把包装类自动的变成其对象的基本数据类型。

由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。

list.add(i); // 这种集合添加int类型,也会触发自动装箱

5.5 基本类型与字符串之间的转换

Java是强类型的语言:每种数据在Java中都有各自的数据类型,在计算的时候,如果不是同一种数据类型,是无法直接计算的。

5.5.1 基本类型转换为String

  • 转换方式
  • 方式一:直接在数字后加一个空字符串
  • 方式二:通过String类静态方法valueOf()
int num = 100;

String s1 = num + "";
System.out.println(s1);

String s = String.valueOf(num);
System.out.println(s);

5.5.2 String转换成基本类型

除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型:

  • public static byte parseByte(String s):将字符串参数转换为对应的byte基本类型。
  • public static short parseShort(String s):将字符串参数转换为对应的short基本类型。
  • public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
  • public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
  • public static float parseFloat(String s):将字符串参数转换为对应的float基本类型。
  • public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。
  • public static boolean parseBoolean(String s):将字符串参数转换为对应的boolean基本类型。

代码使用(仅以Integer类的静态方法parseXxx为例):

  • 转换方式
    • 方式一:先将字符串数字转成Integer,再调用valueOf()方法
    • 方式二:通过Integer静态方法parseInt()进行转换
String s = "100";
int i = Integer.parseInt(s);

//方式2 麻烦:String --- Integer --- int
Integer i1 = Integer.valueOf(s);
int i2 = i1.intValue();

注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常。

麻了的练习:

需求:

​ 自己实现parseInt方法的效果,将字符串形式的数据转成整数。

要求:(1)字符串中只能是数字不能有其他字符

(2)最少一位,最多10位

(3)0不能开头

我的思路:startsWith,string.length.....

合理思路:str.matches("[1-9]\\d{0,9}");

char a = '1'类型直接赋值int是ASCII码49,可以直接 - 48或者 - '0'

练习2:

需求:

​ 判断任意的一个年份是闰年还是平年要求:用JDK7和JDK8两种方式判断提示:二月有29天是闰年一年有366天是闰年

思路,用Calendar,LocalDate获取该年3月1日的日期对象,用两种类的各自方法,将日期往前推一天,那天就是2月底的日期,看这个日期是28还是29。

JDK8还有个方法,速通,LocalDate的isLeapYear();

public class Test5 {
    public static void main(String[] args) {
        /*
            判断任意的一个年份是闰年还是平年要求:用JDK7和JDK8两种方式判断提示:
            二月有29天是闰年一年有366天是闰年
        */

        //jdk7
        //我们可以把时间设置为2000年3月1日
        Calendar c = Calendar.getInstance();
        c.set(2000, 2, 1);
        //月份的范围:0~11   !!!!!
        //再把日历往前减一天
        c.add(Calendar.DAY_OF_MONTH, -1);
        //看当前的时间是28号还是29号?
        int day = c.get(Calendar.DAY_OF_MONTH);
        System.out.println(day);

        //jdk8
        //月份的范围:1~12
        //设定时间为2000年的3月1日
        LocalDate ld = LocalDate.of(2001, 3, 1);
        //把时间往前减一天
        LocalDate ld2 = ld.minusDays(1);
        //获取这一天是一个月中的几号
        int day2 = ld2.getDayOfMonth();
        System.out.println(day2);

        //true:闰年
        //false:平年
        System.out.println(ld.isLeapYear());
    }
}

day21 算法

具体算法内容,后续另外开个markdown。

1.递归

递归指的是方法中调用方法本身的现象。

注意点:递归一定要有出口,否则就会出现内存溢出。

作用:把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题求解。

  • 例题:

利用递归求1-100之间的和

100 + 99 + 98 + 97 + 96 + .... + 2 + 1

找出口,找规律

  • 思路:
大问题拆成小问题
1-100之间的和 = 100 + (1-99之间的和)
1-99之间的和 = 99 + (1-98之间的和)
1-98之间的和 = 98 + (1-97之间的和)
。。。
1-2之间的和 = 98 + (1-1之间的和)
1-1之间的和 = 1   (递归的出口)
  • 代码:
public static void main(String[] args) {

    int sum = getSum(100);
    System.out.println(sum);
}

private static int getSum(int i) {

    if (i == 1) {
        return 1;
    }
    return i + getSum(i-1) ;
}
  • 递归内存图:

递归算法

递归就是快速排序的原理。

1.1 练习——不死神兔

有一个很有名的数学逻辑题叫做不死神兔问题,有一对兔子,从出生后第三个月起每个月都生一对兔子小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问第十二个月的兔子对数为多少?

兔子

以根节点为例:白心为小兔子,黑心为老兔子,第一个月是小兔子,第二个月长成老兔子,第三个月就可以生了,后面每个月都生,如图最左黑色分支。

规律:从第三个月开始,每个月的数量为前面两个月的数量和。(解法2:利用数组,为索引0和1赋初始值,利用循环,为后面数据赋值。)

public static void main(String[] args) {
    //

    int sum = countRabbit(1, 12);
    System.out.println(sum);   // 144
}

private static int countRabbit(int startCount, int month) {
    if (month == 1 | month == 2) {
      return startCount;
    }
    // 下面没必要,countRabbit(1) + countRabbit(2) == 两倍startCount
    /*if (month == 3) {
        return startCount * 2;  // 第0轮循环,即还没开始生,只有startCount对兔子。
    }*/

    return countRabbit(startCount, month-1) + countRabbit(startCount, month-2);
}

1.2 练习——猴子吃桃

有一堆桃子,猴子第一天吃了其中的一半,并多吃了一个!以后每天猴子都吃当前剩下来的一半,然后再多吃一个,第10天的时候(还没吃),发现只剩下一个桃子了,请问,最初总共多少个桃子?

// 思路
// 10   9   8    7   6  5  4  3  2  1  0
//  1   4   10   21
day 9 = (day10 + 1) * 2 = 4;  day9 吃了 2 + 1 = 3个
每一天桃子的数量都是后一天数量+1, 乘以2
public class PeachTest {

    public static void main(String[] args) {

        int sum = monkeyPeach(1, 1, 10);
        System.out.println(sum);  // 1534

    }

    /**
     *
     * @param getWhichDayNum 你想获取哪一天的数量
     * @param peachNum 你所知道的 某天的桃子数量
     * @param knowWhatDayNum 你所知道的桃子数量在哪一天
     * @return 想获取哪天的桃子数量
     */
    private static int monkeyPeach(int getWhichDayNum, int peachNum, int knowWhatDayNum) {  // day = 1 获取第
        if (getWhichDayNum <= 0 || getWhichDayNum >= 11) {
            System.out.println("当前时间错误");
            return -1;
        }

        // 反向递归
        if (getWhichDayNum == knowWhatDayNum) {
            return peachNum;
        }
        // 每一天桃子的数量都是后一天数量+1, 乘以2
        return (monkeyPeach(getWhichDayNum + 1, peachNum, knowWhatDayNum) + 1) * 2;
    }
}

1.3 练习 ——爬楼梯

楼梯

1层台阶:只能选择迈一步,一种爬法

2层台阶:(1)一步两格迈上去(2)一步一步两次迈上去,两种爬法

19个台阶到20个台阶,只能走一步,就一种。

18个台阶到20个台阶,不同爬法就一种(跨两级上去)。跨一级的话就到了19级台阶,那么这种情况已经被上面(19个台阶的爬法)包括。

所以20个台阶的爬法为 19个台阶的爬法 + 18个台阶的爬法,

无需考虑17个台阶的爬法,从17阶无论跨一步还是跨两步,都被19阶和18阶所包含。

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

        System.out.println(getCount(20));  // 10946
        System.out.println(getCount(7));  // 21

    }

    public static int getCount(int n) {
        if (n == 1) {
            return 1;
        }
        if (n == 2) {
            return 2;
        }

        return getCount(n - 2) + getCount(n - 1);
    }
}

// =======================================================================================
// 扩展,楼梯数依旧为20,但小明也能一次跨3个台阶,求爬法数量。
public static void main(String[] args) {

    System.out.println(getCount(20));  // 121415

}

public static int getCount(int n) {
    // 当n==3时,下面就是getCount0+1+2, 所以记得补n == 0
    if (n == 1 || n == 0) {
    // if (n == 1) {
        return 1;
    }
    if (n == 2) {
        return 2;
    }
    // if (n == 3) {     // n = 3不用添加,此时就是 getCount(0 + 1 + 2)
    //      return 4;
    // }

    return getCount(n - 3) + getCount(n - 2) + getCount(n - 1);
}

2. Arrays

Arrays

int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] arr1 = {1, 2, 3, 4, 6, 7, 8, 9, 10};
System.out.println(Arrays.toString(arr));   // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// 二分查找前提,数组中的元素必须有序,而且是升序的。
// 如果查找的元素存在,返回索引
System.out.println(Arrays.binarySearch(arr, 4));  // 3
// 如果查找的元素不存在,返回的是 -应该的插入点-1
// 为什么要减1?
// 如果此时查找数字0,返回的值如果是 -插入点,则结果为 -0 = 0, 就有问题
System.out.println(Arrays.binarySearch(arr, 20));   // -11
System.out.println(Arrays.binarySearch(arr1, 5));  // -5   - ( 4 + 1)

int[] newArr1 = Arrays.copyOf(arr, 11);  // 如果索引小于原数组,则copy到长度位置停止,大于则补默认初始化值
System.out.println(Arrays.toString(newArr1));  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0]

int[] newArr2 = Arrays.copyOfRange(arr, 3, 7);  // 不包括 索引 7
System.out.println(Arrays.toString(newArr2));   // [4, 5, 6, 7]

Arrays.fill(arr, 100);
System.out.println(Arrays.toString(arr));  // [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]

// 默认情况下,进行升序排序,底层使用快速排序
int[] arr3 = {10, 2, 3, 5, 6, 1, 7, 8, 4, 9};
Arrays.sort(arr3);
System.out.println(Arrays.toString(arr3));   // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

2.1 Arrays.sort 自定义规则

底层原理:

  • 利用插入排序 + 二分查找的方式进行排序的。
  • 默认把 0 索引的数据当做是有序的序列,1索引到最后认为是无序的序列。
  • 遍历无序的序列得到里面的每一个元素,假设当前遍历得到的元素是A元素
  • 把A往有序序列中进行插入,在插入的时候,是利用二分查找确定A元素的插入点。
  • 拿着A元素,跟插入点的元素进行比较,比较的规则就是compare方法的方法体
  • 如果方法的返回值是负数,拿着A继续跟前面的数据进行比较
  • 如果方法的返回值是正数,拿着A继续跟后面的数据进行比较
  • 如果方法的返回值是0,也拿着A跟后面的数据进行比较
  • 直到能确定A的最终位置为止。

简单理解:

  • o1 - o2 升序排序
  • o2 - o1 降序排序
// Arrays.sory() 自定义排序规则:
// 参数1:要排序的数组,参数2:排序的规则
// 细节:只能给引用数据类型进行排序,如果数组是基本数据类型的,需要变成其包装类
Integer[] arr = {2, 3, 1, 5, 6, 7, 8, 4, 9};
// 第二个参数,是一个接口,所以需要传递这个接口的实现类对象,作为排序的规则,匿名内部类

//compare方法的形式参数:
//参数一 o1: 表示在无序序列中,遍历得到的每一个元素
//参数二 o2: 有序序列中的元素
Arrays.sort(arr, new Comparator<Integer>() {

    // 返回值:
    // 负数:表示当前要插入的元素是小的,放在前面
    // 正数:表示当前要插入的元素是大的,放在后面
    // 0:表示当前要插入的元素跟现在的元素比是一样的,也会放在后面

    // 简单理解:
    // o1 - o2 升序排序
    // o2 - o1 降序排序
    @Override
    public int compare(Integer o1, Integer o2) {
        System.out.println("-------------------------------");
        // Integer[] arr = {2, 3, 1, 5, 6, 7, 8, 4, 9};
        // 刚开始 2是有序序列, 3是无序序列第一个
        System.out.println("o1 : " + o1);  // 3  1
        System.out.println("o2 : " + o2);  // 2  3
        return o1 - o2;          //o1 - o2    1  -2
    }
});

System.out.println(Arrays.toString(arr));  // [1, 2, 3, 4, 5, 6, 7, 8, 9]

运行结果:

-------------------------------
o1 : 3
o2 : 2
-------------------------------
o1 : 1
o2 : 3
-------------------------------
o1 : 1     (结果和上面相同可能是因为 二分查找 )
o2 : 3
-------------------------------
o1 : 1
o2 : 2
-------------------------------
o1 : 5     (5没有和3比,直接和二分查找后2比)
o2 : 2
-------------------------------
o1 : 5
o2 : 3
-------------------------------
o1 : 6
o2 : 3
-------------------------------
o1 : 6
o2 : 5
-------------------------------
o1 : 7
o2 : 3
-------------------------------
o1 : 7
o2 : 6
-------------------------------
o1 : 8
o2 : 5
-------------------------------
o1 : 8
o2 : 7
-------------------------------
o1 : 4
o2 : 5
-------------------------------
o1 : 4
o2 : 2
-------------------------------
o1 : 4
o2 : 3
-------------------------------
o1 : 9
o2 : 5
-------------------------------
o1 : 9
o2 : 7
-------------------------------
o1 : 9
o2 : 8
[1, 2, 3, 4, 5, 6, 7, 8, 9]

3. Lambda表达式

  • 例子:
Arrays.sort(arr, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
});

// 上面 简化 成下面
Arrays.sort(arr, (Integer o1, Integer o2) -> {
        return o2 - o1;
});

// 最终简化
Arrays.sort(arr, (o1, o2) -> o2 - o1);
  • 函数式编程
    函数式编程(Functional programming)是一种思想特点函数式编程思想,忽略面向对象的复杂语法,强调做什么,而不是谁去做(面向对象)。
Lambda是JDK8开始后的一种新语法形式

() -> {}

注意点:

  • Lambda表达式可以用来简化匿名内部类的书写
  • Lambda表达式只能简化函数式接口的匿名内部类的写法,抽象类不行
  • 函数式接口:
    • 有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加@FunctionalInterface注解
public class LambdaDemo {
    public static void main(String[] args) {
        // 1.利用匿名内部类的形式去调用下面的方法
        // 调用一个方法的时候,如果方法的形参是一个接门,那么我们要传递这个接口的实现类对象
        // 如果实现类对象只要用到一次,就可以用匿名内部类的形式进行书写

        method(new Swim() {
            @Override
            public void swimming() {
                System.out.println("正在游泳");
            }
        });
        // ================================================================
        // 2.利用Lambda表达式进行改写
        method(() -> {
            System.out.println("I am Swimming");  // {}可省略
        });

    }

    public static void method(Swim s) {
        s.swimming();
    }
}

@FunctionalInterface
interface Swim{
    public abstract void swimming();
}

lambda的省略规则:

  • 参数类型可以省略不写。
  • 如果只有一个参数,参数类型可以省略,同时()也可以省略。
  • 如果Lambda表达式的方法体只有一行,大括号,分号,return可以省略不写,需要同时省略。
// 完整格式
Arrays.sort(arr, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
});

// Lambda完整格式
Arrays.sort(arr, (Integer o1, Integer o2) -> {
    return o1 - o2;
});

// Lambda省略写法。和完整格式一一对应(去除参数类型)
// 如果Lambda表达式的方法体只有一行,大括号,分号,return可以省略不写,需要同时省略。
Arrays.sort(arr, ( o1,  o2) ->   o1 - o2 );

day23

1. 泛型

1.1泛型概述

  • 泛型是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
  • 泛型的好处
    1. 统一数据类型
    2. 把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来。
  • 注意:
    1. 泛型只能支持引用数据类型,不能写基本数据类型
    2. 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
    3. 如果不写泛型,类型默认是object
  • 泛型的定义格式
    • \<类型>: 指定一种类型的格式.尖括号里面可以任意书写,一般只写一个字母.例如: \<E> \<T>
    • \<类型1,类型2…>: 指定多种类型的格式,多种类型之间用逗号隔开.例如: \<E,T> \<K,V>
// 如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型
// 此时可以往集合添加任意的数据类型。
// 带来一个坏处,我们在获取数据的时候,无法使用它的特有行为。

// 泛型:可以在添加数据的时候就把类型进行统一。
// 而且我们在获取数据的时候,也省的强转了,非常的方便。
ArrayList list = new ArrayList();

list.add(123);
list.add("hello");
list.add(new Student());

System.out.println(list);

for (Object o : list) {
    if (o instanceof Integer d){
        System.out.println(d);    // 123
    } else if (o instanceof String d) {
        System.out.println(d.length());   // 5
    }
}
  • Java中的泛型是伪泛型

传入还是Object类

伪

1.2 泛型的继承性

泛型不具备继承性,但是数据具备继承性

public class GenericsDemo5 {

    public static void main(String[] args) {
        /**
         * 泛型不具备继承性,但是数据具备继承性
         */

        // 创建集合的对象
        ArrayList<Ye> list1 = new ArrayList<>();
        ArrayList<Fu> list2 = new ArrayList<>();
        ArrayList<Zi> list3 = new ArrayList<>();

        // 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
        list1.add(new Fu());
        list1.add(new Zi());

        method(list1);
        // method(list2);   // 报错
        // method(list3);   // 报错

    }

    public static void method(ArrayList<Ye> list) {
    // public static <E> void method(ArrayList<E> list) {  // 此时没报错
    // 弊端:利用泛型方法有一个小弊端,此时他可以接受任意的数据类型

    // public static void method(ArrayList<?> list) {   // 也没报错
    }
}

class Ye {
}

class Fu extends Ye {
}

class Zi extends Fu {
}

虽然上面如果使用泛型E,ArrayList\<E> list,虽然可以传递不同类型,但无法控制类型。

如果我们希望只能传递Ye Fu Zi。

此时我们可以使用泛型的通配符

?表示不确定的类型

? extends E:表示可以传递E或者E所有的子类类型

? super E:表示可以传递E或者E所有的父类类型

public static void keepPet(ArrayList<? extends Cat> list) {
    for (Cat cat : list) {
        cat.eat();
    }
}

应用场景:

  1. 如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
  2. 如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符泛型的通配符。

关键点: 可以限定类型的范围。

2. 泛型类

使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义都带有泛型的类。

// 格式:
修饰符 class 类名<类型> {

}

public class ArrayList<E> {
    // 创建该类对象时,E就确定类型
}

此处E可以理解为变量,但是,不是用来记录数据的,而是记录数据的类型,可以写成:T(type)、E(element)、K(key)、V(value)等

  • 代码示例:
public class MyArrayList<E> extends ArrayList<E>{

    Object[] obj = new Object[10];
    int size;

    public boolean add(E e) {
        obj[size] = e;
        size++;
        return true;
    }

    public E get(int index) {
        return (E)obj[index];
    }

    @Override
    public String toString() {
        return Arrays.toString(obj);
    }
}

3.泛型方法

方法中形参类型不确定时,可以使用类名后面定义的泛型\<E>, 例如上面的add(E e)方法

方案1:使用类名后面定义的泛型(所有方法都适用)

方案2:在方法申明上定义自己的泛型(只有本方法能用)

修饰符 <类型> 返回值类型 方法名(类型 变量名) {

}

public <T> void show(T t) {
    // 此处T可以理解为变量,但不是用来记录数据的。
}
  • 代码示例:
// 定义一个工具类:ListUtil
// 类中定义一个静态方法addAll,用来添加多个集合的元素。
public class ListUtil {

    private ListUtil(){

    }

    /**
     * 参数一:集合
     * 参数二——最后,要添加的元素
     */
    public static <E> void addAll(ArrayList<E> list, E e1, E e2, E e3, E e4) {
        list.add(e1);
        list.add(e2);
        list.add(e3);
        list.add(e4);
    }

    public static <E> void addAll2(ArrayList<E> list, E...e) {
        for (E e1 : e) {
            list.add(e1);
        }
    // Collections.addAll(list, e);
    }

}

4. 泛型接口

修饰符 interface 接口名 <类型> {

}

public interface List<E> {

}

如何使用一个带泛型的接口:

  • 方法1:实现类给出具体类型
public class MyArrayList2 implements List<String> {
    // @Override.....
}

MyArrayList2 list = new MyArrayList2();
  • 方法2:实现类延续泛型,创建对象时再确定
public class MyArrayList3<E> implements List<E> {}

MyArrayList3<String> list = new MyArrayList3<>();

day25 可变参数补充

JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化.

格式:

修饰符 返回值类型 方法名(参数类型... 形参名){  }

底层:其实就是一个数组

好处:在传递数据的时候,省的我们自己创建数组并添加元素了,JDK底层帮我们自动创建数组并添加元素了

定义方法,可以计算1/2/3/....个数据的和

public static void main(String[] args) {
    System.out.println(getSum(1, 2, 3, 4, 5));
    System.out.println(getSum(1, 2, 3, 4));
    System.out.println(getSum(1, 2, 3));
    System.out.println(getSum(1, 2));
    System.out.println(getSum(1));
    System.out.println(getSum());  // 0
}

public static int getSum(int...num) {
    int sum = 0;
    for (int i : num) {
        sum += i;
    }
    return sum;
}

注意:

  1. 一个方法只能有一个可变参数(否则,无法确定方法实参归于那个可变参数)
  2. 如果方法中有多个参数,可变参数要放到最后。放到最后,(前面的一一分配,剩下的可变参数全吃)

应用场景: Collections

在Collections中也提供了添加一些元素方法:

public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。

ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 8);
System.out.println(list);  // [1, 2, 3, 4, 5, 8]