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)				// 按照四舍五入返回最接近参数的int类型的值
public static int max(int a,int b)				// 获取两个int值中的较大值
public static int min(int a,int b)				// 获取两个int值中的较小值
public static double pow (double a,double b)	// 计算a的b次幂的值
public static double random()					// 返回一个[0.0,1.0)的随机值
  • 绝对值方法abs:
        System.out.println(Math.abs(-2147483648)); // -2147483648 超过int类型最大值
        System.out.println(Math.absExact(-2147483648)); //ArithmeticException 可以报错
  • 向上取整 ceil:向数轴正方向进一
        System.out.println(Math.ceil(23.45)); //24
        System.out.println(Math.ceil(-23.45)); //-23
  • 向下取整 floor:向数轴负方向进一
        System.out.println(Math.floor(23.45));//23
        System.out.println(Math.floor(-23.45));//-24.0

System

public static void exit(int status) //停止虚拟机
public static long currentTimrMillis() //获取1970 1 1 8:00:000 至此的毫秒数
public static void arraycopy(int[] src,int src_begin,int[] dest,int dest_begin,int count) //数组拷贝
public static void gc()  //建议启动垃圾回收器
  • 数组拷贝
        int[] arr1 = {1,2,3,4,5,6,7};
        double[] arr2 = new double[10];

        System.arraycopy(arr1,0,arr2,0,arr1.length);
        System.out.println(Arrays.toString(arr2)); //ArrayStoreException

如果是基本数据类型,必须保持一致;如果是引用数据类型,子类数据可以赋值给父类数据

Runtime

该类中的大多数方法不是静态的,需要获取当前系统的运行环境对象;而这个对象不能直接new,需要调用public static RunTime getRunTime()方法获取

这代表了不管在哪个类中获取Runtime对象 获取到的都是同一个对象,电脑只有一个运行环境,获取多个对象是没有意义的

        Runtime r1 = Runtime.getRuntime();
        Runtime r2 = Runtime.getRuntime();
        System.out.println(r1 == r2);//true
public static Runtime getRuntime()		//当前系统的运行环境对象
public void exit(int status)			//停止虚拟机(System调用的就是这个方法)
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命令
  • System类:

System.out.println(Runtime.getRuntime().availableProcessors()); //CPU的线程数:16
System.out.println(Runtime.getRuntime().maxMemory()/1024/ 1024 + "MB"); //JVM可从系统中获取的总内存大小:3GB
System.out.println(Runtime.getRuntime().totalMemory()/1024/1024 + "MB"); // JVM已经获得的总内存大小:256MB
System.out.println(Runtime.getRuntime().freeMemory()/1024 / 1024 + "MB"); //JVM剩余内存大小:252MB
/*当前程序使用了:256 - 252 = 4MB*/
  • 运行cmd命令:
shutdown -s # 默认在一分钟后关机 -t可以指定关机(s)
shutdown -s -t 3600 # 一小时后关机
# -a 取消关机操作 -r 关机并重启

Object

Object类只能提供无参构造,所以默认提供的super()是无参构造

以下五个方法也是Object类提供给所有子类的虚方法表

public String toString()				//返回该对象的字符串表示形式
public boolean equals(Object obj)		//比较两个对象地址值是否相等;true表示相同,false表示不相同
protected Object clone()    			//对象克隆
public native int hashCode();           //返回哈希值
protected void finalize() throws Throwable //

hashCode()

该方法不是抽象方法,JVM会使用对象的hashcode值提高对hashMap,hashtable存取对象的使用效率;默认实现是:不同对象调用hashcode方法返回的哈希值就是不同的;可以等同看作是对象的内存地址(本质上不是内存地址,是某种策略生成的)

Object o = new Object();
int hashCodeValue = o.hashCode();
System.out.println(hashCodeValue);

hashCode的生成方式

static inline intptr_t get_next_hash(Thread* current, oop obj) {
  intptr_t value = 0;
  if (hashCode == 0) {
    // This form uses global Park-Miller RNG.
    // On MP system we'll have lots of RW access to a global, so the
    // mechanism induces lots of coherency traffic.
    value = os::random(); //1. 随机数作为hashCode
  } else if (hashCode == 1) {
    // This variation has the property of being stable (idempotent)
    // between STW operations.  This can be useful in some of the 1-0
    // synchronization schemes.
    intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3; //读取内存地址
    value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; //2. 内存地址结合随机数运算作为hashCode
  } else if (hashCode == 2) {
    value = 1;            // for sensitivity testing  3. 固定为1
  } else if (hashCode == 3) {
    value = ++GVars.hc_sequence;  // 4. 自增的序列作为hashCode
  } else if (hashCode == 4) {
    value = cast_from_oop<intptr_t>(obj); // 5. 对象的内存地址作为hashCode
  } else {
    // Marsaglia's xor-shift scheme with thread-specific state
    // This is probably the best overall implementation -- we'll
    // likely make this the default in future releases.
      
    //6. 随机数算法
    unsigned t = current->_hashStateX;
    t ^= (t << 11);
    current->_hashStateX = current->_hashStateY;
    current->_hashStateY = current->_hashStateZ;
    current->_hashStateZ = current->_hashStateW;
    unsigned v = current->_hashStateW;
    v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
    current->_hashStateW = v;
    value = v;
  }

  value &= markWord::hash_mask;
  if (value == 0) value = 0xBAD;
  assert(value != markWord::no_hash, "invariant");
  return value;
}

默认使用的是第六种 随机数算法,可以根据如下方式改变hashCode的生成方式:

-XX:+UnlockExperimentalVMOptions 解锁实验性质的虚拟机选项

-XX:hashCode=? 指定hashCode生成方式

toString()

作用是把一个Java对象转换成字符串表示形式,默认实现:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

测试:

class MyDate{
    private int year;
    private int month;
    private int day;
    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public String toString() {//覆盖toString达到想要的输出结果
        return this.year + " - " + this.month + " - " + this.day;
    }
}
public class toStringTest {
    public static void main(String[] args) {
        MyDate myDate = new MyDate(1970,1,1);
        System.out.println(myDate);
        System.out.println(myDate.toString());
        //输出引用时自动调用该引用的valueOf方法,valueOf会调用toString
    }
}
  • System.out.println():

获取到的out对象是PrintStream类型对象,该类有println方法:

public void println(Object x) {
    String s = String.valueOf(x);
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(s));
    } else {
        synchronized (this) {
            print(s);
            newLine();
        }
    }
}

可以看出,把引用x传给了String类的valueOf方法:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

println中调用了valueOf方法,valueOf方法返回了该对象的toString结果

equals()

public boolean equals(Object obj) {
    return (this == obj);
}

目的是判断两个对象是否相等,而Object类中默认使用 == 判断,而 == 判断的是两个对象的内存地址;需要重写

MyDate myDate1 = new MyDate(1970,1,1);
MyDate myDate2 = new MyDate(1970,1,1);
System.out.println(myDate1 == myDate2);
System.out.println(myDate1.equals(myDate2));

此时的MyDate类继承Object类,但是没有重写equals方法,调用的时候还是Object类中的equals方法,所以结果都是false

重写Student中的equals方法:

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        return s.age == this.age && s.name.equals(this.name); //String类型不能用==比较
    }

但是这个方法会有两个问题:

  1. obj == null 的结果已经包含在了 obj instanceof 的判断中了
  2. 对于String类型的参数比较而言,s.name.equals()可能会发生空指针异常

改进之后的:

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        return obj instanceof Student s && s.age == this.age && Objects.equals(s.name,this.name);
    }

Objects.equals()方法会对参数进行非空判断

String类已经重写了toString和equals

比较两个字符串不能用 == 必须用equals

基本数据类型用 == 判断;引用数据类型用equals判断。

public class StringEqualsTest {
    public static void main(String[] args) {
        String s1 = "123";
        String s2 = "123";

        System.out.println(s1 == s2);//直接创建的字符串用 == 比较没问题
        
        String s3 = new String("123");
        String s4 = new String("123");
        System.out.println(s3 == s4);//用构造方法创建的字符串用 == 比较就不行了

        System.out.println(s3.equals(s4));//但是equals比较就没问题

        System.out.println(s3);//直接输出引用就可以输出字符串,说明String重写了toString方法
    }
}

String类重写的equals方法

  • StringBuilder、StringJoiner都没有重写equals方法,比较这些内容是否相等需要toString

finalize()

protected void finalize() throws Throwable {
    System.out.println("being finalized");
}

finalize方法只有一个方法体,里面没有代码,这个方法是protected修饰的;

这个方法不需要手动调用,JVM的垃圾回收器负责调用,当一个Java对象即将被GC回收的时候,GC调用finalize方法;

finalize方法实际上是一个垃圾销毁时机(类似于静态代码块的类加载时机,实例代码块的对象创建时机),如果在对象销毁前执行一段代码的话,这段代码要写在finalize当中。

提示:Java中的GC不是轻易启动的,有时候垃圾太少或者没到实际有可能就不会启动

测试:

public class FinalizeTest {
    public static void main(String[] args) {
        Person p = new Person();
        p = null;

        for (int i = 0; i <= 10; i++){
            Person person = new Person();
            person = null;//制造垃圾
        }

        System.gc();//建议启动GC
        System.out.println("main over");
    }
}
class Person{
    @Override
    protected void finalize() throws Throwable {
        System.out.println("being finalized");
    }
}

clone()

  • 浅克隆:

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

直接调是不行的,Object源码:

对于Object类的clone(),只能在本包/子类中调用,我们是不能在java.lang包下编写代码的,只能在子类中覆盖这个方法再调用:

子类种调用的含义是,在子类中的方法内可以直接调用,不是通过子类引用调用

@Override
protected Object clone() throws CloneNotSupportedException {
    return super.clone();
}

覆盖之后的权限修饰符就是针对子类而言的,但是对于子类中的clone方法来说,也只能被同包下的测试类及子类的子类访问

同时要让本类实现接口:Cloneable

这是一个空的标记性接口,实现了Cloneable当前类的对象就可被克隆

    public static void main(String[] args) throws CloneNotSupportedException {
        User u1 = new User(1,"zhangsan","1234qwer","girl11",new int[]{1,2,3});
        User u2 = (User) u1.clone();
    }

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

  • 深克隆会将引用数据类型再创建一个新的对象,将数组中的内容完全复制一份再拷贝到新对象当中

需要改变的就是重写的clone方法:

@Override
protected Object clone() throws CloneNotSupportedException {
    int[] newArr = new int[this.getData().length];
    for (int i = 0; i < newArr.length; i++) {
        newArr[i] = getData()[i];
    }
    User user = (User) super.clone();
    user.setData(newArr);
    return user; 
}

可以避免两个引用都指向一个数组对象

使用第三方jar包

        Gson gson = new Gson(); //创建工具对象
        String s = gson.toJson(u1); //将对象转换为字符串
        System.out.println(s); 		//{"id":1,"username":"zhangsan","password":"1234qwer","path":"girl11","data":[1,2,3]}

再进行对象克隆就转换为对象了

        User user = gson.fromJson(s, User.class);
        System.out.println(user);

Objects

equals(Object a,Object b)

        Student s1 = null;
        Student s2 = new Student("zhangsan",11);
        
        boolean result = s1.equals(s2);
        System.out.println(result); //NullPointerException

按照之前所学需要在此进行非空判断,Java提供了非空判断Objects.equals

        Student s1 = null;
        Student s2 = new Student("zhangsan",11);

        boolean result = Objects.equals(s1,s2);
        System.out.println(result); //false

Objects.equals源代码:

会调用bean类的equals方法,需要确保该方法被重写了

其他方法

public static boolean isNull(Object obj)					// 判断对象是否为null
public static boolean nonNull(Object obj)					// 判断对象是否不为null

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所提供的值

包装类

假设有需求:调用doSome方法时传递一个数字进去:

public static void doSome(Object obj){
    System.out.println(obj);//输出引用自动调用toString方法
}

但是数字属于基本数据类型,方法的参数是Object类型;可见doSome方法无法接受基本类型的数字,可以传递一个数字对应的包装类进去:

class MyInt{
    int num;

    public MyInt(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return String.valueOf(num);//把数字转换为字符串
    }
}

在main函数里就可以这样调用:

public static void main(String[] args) {
    MyInt myInt = new MyInt(10);
    doSome(myInt);
}

这种包装类实际上SUN已经写好了:

为了编程方便,java对于八种基本数据类型提供了包装类:

基本数据类型 java.lang.包装类型
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

classDiagram class Object{ } class Number{ } class Boolean{ } class Character{ } class Byte{ } class Short{ } class Integer{ } class Long{ } class Float{ } class Double{ } Object <|– Number Object <|– Boolean Object <|– Character Number <|– Byte Number <|– Short Number <|– Integer Number <|– Long Number <|– Float Number <|– Double

其中六种数字类的父类都是Number,说明他们都属于数字,而Boolean和Character不是数字

包装类:基本数据类型对应的引用类型。

从内存角度理解:

Number

Number类是一个抽象类,无法实例化。

由于Number类是六种数字类的父类,先看Number类中的方法:

byte byteValue();//以 byte 形式返回指定的数值。
short shortValue();//以 short 形式返回指定的数值。

abstracte int intValue();//以 int 形式返回指定的数值。
abstracte long longValue();//以 long 形式返回指定的数值。
abstracte float floatValue();//以 float 形式返回指定的数值。
abstracte double doubleValue();//以 double 形式返回指定的数值。

这些方法在它的子类中都有,负责把包装类转换成各种基本数据类型(也就是拆箱)

public class IntegerTest02 {
    public static void main(String[] args) {
        Integer i = new Integer(123);//装箱
        System.out.println(i.floatValue());//拆箱  123.0
    }
}

拆箱本质上就是转换为各种不同的数据类型

通过构造方法的包装达到了:基本数据类型向引用数据类型的转换

Integer类的构造方法:

  • Integer(int)
  • Integer(String) 将String类型的数字转换成包装类型,调用了parseInt方法
public static void main(String[] args) {
    Integer x = new Integer(123);// int ---> Integer
    Integer y = new Integer("123"); //String ---> Integer

    System.out.println(y);//说明toString方法已经被重写了
    
    Double d = new Double(1.23);//double ---> Double
    Double e = new Double("1.23");//String ---> Double
}

java9之后不建议使用这个构造方法了

如果parseInt解析的不是字符串类型的数字:

Integer y = new Integer("abc"); 

会出现:NumberFormatException,表示数字格式化异常

其中的parseInt方法:

本质上就是对每一个字符进行判断

equals和toString

public class IntegerEquals {
    public static void main(String[] args) {
        Integer x = 1;
        System.out.println(x.toString());
        Integer y = 1;
        System.out.println(x.equals(y));
    }
}

说明Integer重写了toString和equals

toString:

获取到char数组,再将char转换为字符串

注意,将基本类型的数据转换为字符串类型:

  • Double.toString(double b) //静态方法
  • Double b = 1.0; sout(b.toString()) //实例方法

equals:

将Integer内部记录的value和拆箱后的value进行比较

Comparable

通过源码可以得知Number的六个子类实现了Comparable接口,说明可以通过compareTo方法比较Integer的大小,Integer数组可以使用Arrays.sort默认升序排列

public class IntegerCompareTo {
    public static void main(String[] args) {
        Integer m = new Integer(200);
        Integer n = new Integer(100);
        Integer k = new Integer(200);

        System.out.println(m.compareTo(n));//1 大于
        System.out.println(m > n);         //true 大于
        System.out.println(m.compareTo(k));//0 等于
        System.out.println(m == k);        //false 比较相等要用equals
        System.out.println(m.equals(k));   //true
    }
}

说明在比较大小的时候可以使用equals > < 或者compareTo方法(== 比较的是对象的内存地址)

常量

通过访问常量可以得到包装类的最大值或者最小值:

public class IntegerStaticFinal {
    public static void main(String[] args) {
        System.out.println(Integer.MAX_VALUE);//2147483647
        System.out.println(Integer.MIN_VALUE);//-2147483648

        System.out.println(Byte.MAX_VALUE);//127
        System.out.println(Byte.MIN_VALUE);//-128
    }
}

自动装箱与自动拆箱

JDK1.5之前:获取Integer对象的方式:

  1. 构造方法
方法名 说明
public Integer(int value) 根据传递的整数创建一个Integer对象
public Integer(String s) 根据传递的字符串创建一个Integer对象
  1. 静态方法valueOf()
方法名 说明
public static Integer valueOf(int i) 根据传递的整数创建一个Integer对象
public static Integer valueOf(String s) 根据传递的字符串创建一个Integer对象
public static Integer valueOf(String s,int radix) 根据传递的字符串和进制创建一个Integer对象

这两种方式的区别:

        Integer i1 = Integer.valueOf(127);
        Integer i2 = Integer.valueOf(127);
        System.out.println(i1 == i2); //true

        Integer i3 = Integer.valueOf(128);
        Integer i4 = Integer.valueOf(128);
        System.out.println(i3 == i4); //false

        Integer i5 = new Integer(127);
        Integer i6 = new Integer(127);
        System.out.println(i5 == i6); //false

        Integer i7 = new Integer(128);
        Integer i8 = new Integer(128);
        System.out.println(i7 == i8); //false

关于valueOf()方法:

对i进行判断:在某个范围当中就从IntegerCache中返回一个对象,而IntegerCache:

IntegerCache是一个私有内部类,该类内部有一个cache[]数组,这个数组被static final修饰表示常量,数组中每个元素都是Integer类型,这个数组被称为整数型常量池。在Integer类进行加载的时候会在堆(Java7之前是方法区)中生成一个整数型常量池,这个常量池的范围从 -128 ~ 127 ,也就是说明使用valueOf方法获取Integer对象时如果数字没有超过这个范围会直接从整数型常量池中拿取这个Integer对象,而不会用new新建对象。

VM options ==> -XX:AutoBoxCacheMax=?

可以更改常量池大小

对于String类型参数的valueOf方法,其实就是调用了一次parseInt方法转换为int类型:

在JDK1.5之前,如果要对包装类进行计算:

Integer i1 = new Integer(1);
Integer i2 = new Integer(2);

//对象是不能直接进行计算的,需要先拆箱
int result = i1.intValue() + i2.intValue();

//装箱
Integer i3 = new Integer(result)

Integer类型的值是由内部的private final int value记录的,不能重新赋值

JDK1.5之后,Java引入了一种机制叫做:自动装箱(auto-boxing)和自动拆箱(auto-unboxing),目的在于简化编程,便于代码的编写,所以在Java9之后Integer类的两个方法都显示已过时了;并且有了自动拆箱之后,Number类的方法就用不着了(如果是转换成对应的基本数据类型的话)

  • 自动装箱:当基本数据类型处于需要对象的环境中就会触发自动装箱机制
  • 自动拆箱:当对象处于需要基本数据类型的环境中,则就会触发自动拆箱机制
public class IntegerAutoBoxing {
    public static void main(String[] args) {
        Integer x = 1; //自动装箱
        int y = x;     //自动拆箱
    }
}

自动拆箱或者自动装箱并不是类型的自动转换,只是在编译阶段的一种语法处理,实际上还是会调用valueOf()方法获取Integer对象

public static void main(String[] args) {
    Integer x = 500;
    
    int y = x;
    
    Integer z = 500;

    System.out.println(x == z);
}

得到的结果是false,这是因为x和z超过了IntegerCache的范围,在valueOf方法中使用构造方法创建了对象,x和z引用保存的内存地址不同,指向了两个不同的内存对象。

Integer z = 1000;

System.out.println(z + 1);//1001

+只能算数字之间的加法,此处没有报错就是自动拆箱机制,同理 + – * / 都是因为自动拆箱机制(但是== 不会)

常用方法

注意:除了Character都有对应的parseXxx方法,进行类型转换

关于整数相关的方法,不能定义在int中,只能定义在Integer类当中

方法名 说明
public static String toBinaryString(int i) 得到二进制(补码)
public static String toOctalString(int i) 得到八进制
public static String toHexString(int i) 得到十六进制
public static int parseInt(String s) 字符串转为int
public static Integer valueOf(String s) 还是调用了parseInt方法

public static int parseInt(String s) throws NumberFormatException

静态方法,类名调用;作用:将字符串参数作为有符号的十进制数解析并返回

int value = Integer.parseInt("123");
System.out.println(value);

public static String toBinaryString(int i)

静态方法;作用:将十进制整数作为无符号二进制数解析并返回

System.out.println(Integer.toBinaryString(163));

public static String toHexString(int i)

静态方法;作用:将十进制整数作为无符号十六进制数解析并返回(也是toString默认实现中的方法)

System.out.println(Integer.toHexString(123));

public static String toOctalString(int i)

静态方法;作用:将十进制整数作为无符号八进制数解析并返回

public static Integer valueOf(int i)

静态方法,将int类型转换为Integer类型

Integer i1 = Integer.valueOf(100);

public static Integer valueOf(String s) throws NumberFormatException

静态方法,将String类型转换为Integer类型

Integer i2 = Integer.valueOf("100");

String int Integer相互转换

/**  
 * String -> int 
 *      Integer.parseInt(String) 
 * int -> String 
 *      int + ""    String.valueOf(int) 
 * String -> Integer 
 *      new Integer(String)   Integer.valueOf(String) 底层都是parseInt  
 * Integer -> String 
 *      String.valueOf    Integer + "" 
 * int -> Integer 
 *      new Integer(int)  auto-boxing  Integer.valueOf(int) 
 * Integer -> int 
 *      auto-unboxing  integer.intValue */

时间类

JDK7 时间类API

GMT与UTC

GMT

格林威治(也称:格林尼治)(Greenwich Mean Time,简称G.M.T.) 时间,之前也叫世界时(Universal Time),也称世界标准时间。是指位于英国伦敦郊区的【皇家格林尼治天文台】的标准时间,是本初子午线上的地方时,是0时区的区时。

众所周知,中国统一用的北京时间是位于东八区(+8)与标准时间相差8小时。什么含义?举个例子:若GMT(英国伦敦的格林威治)现在是上午11点,那中国北京时间现在就是 11 + 8 = 19点(下午7点)。

将这个公式再抽象一下,可表示为:本地时间=GMT+时区差。

  • 时区(Time zone)是地球上的区域使用同一个时间定义

为了克服时间上的混乱,1884年在华盛顿召开了一次国际经度会议(又称国际子午线会议), 会议上制定了全球性的标准时。它规定英国格林威治天文台旧址为全球时间的中心点(零时区),并由它负责维护和计算

  • 除了选定中心点作为时间时之外,它还规定将全球划分为24个时区(东、西各12个时区)

地球绕自转轴自西向东的转动(太阳东起西落),所以东时区的人会比西时区的人早一些看到太阳,从而时间上会早一点

UTC

UTC指的是Coordinated Universal Time- 世界协调时间(又称世界标准时间、世界统一时间),它是以铯原子时作为计量单位的时间,计算结果极其严谨和精密。它比GMT时间更来得精准,误差值必须保持在0.9秒以内,倘若大于0.9秒就会通过位于巴黎的国际地球自转事务中央局发布的闰秒来“解决”。

美国的物理实验市在2014年造出了人类历史上最精确的原子钟,50亿年误差1s,可谓相当靠谱了。中国的铯原子钟也能确保2000万年误差不超过1s。

  • UTC是标准时间参照,像GMT(格林威治时间)、ET(美国东部时间)、PST(太平洋时间)、CST(北京时间)等等都是具体的时区时间。
  • GMT能和UTC直接转换,仅仅是因为碰巧GMT是0时区时间,数值上刚好和UTC是相等的(不需要精确到秒的情况下,二者可以视为相等),看起来一样,但是概念不同。
  • 在日常生活中,我们所使用的时间肯定是本地时间。在只有GMT的时候,本地时间是通过时区计算出来的,而现在UTC才是标准参考,因此采用UTC和偏移量(Offset)的方式来表示本地时间:

这个偏移量可表示为:UTC -或UTC +,后面接小时数,分钟数。如:UTC +9:30表示澳大利亚中央标准时间,UTC +8表示中国标准时间。偏移量常见的表示形式有:±[hh]:[mm]、±hh、±[hh]这三种方式均可。

举个例子:现在UTC时间是10:30z(z表示偏移量=0),那么北京时间现在若是1630 +0800(下午4点半),对应的纽约时间就是0530 -0500(早上5点半)。

注意:在UTC的世界里并无时区的概念,而是偏移量(时间点跟上偏移量才是一个正规的UTC时间),它和时区并无直接关系

偏移量可以精确到分钟级别控制,非常精细化。全球只有24个时区(只能精确到小时),但偏移量有“无数个”。为了方便沟通,时间日期联盟组织把世界主要国家/城市的偏移量汇总起来且起名为Time zone name用于沟通,共有上百个。

构造方法

利用空参构造创建的对象:默认表示当前系统时间。

利用有参构造创建的对象:表示指定的时间。

public class Date{
    private long fastTime; // 当前时间与时间原点的差值(1970年1月1日 00:00:000
    
    public Date(){
		this(System.currentTimeMillis()) //默认当前系统时间
    }
    public Date(long time){
        fastTime = time; //指定的时间
    }
}
        Date d1 = new Date(); // 系统当前时间:Thu Jan 19 22:19:53 CST 2023
        System.out.println(d1);

        Date d2 = new Date(0l); //从时间原点开始过了0毫秒 :Thu Jan 01 08:00:00 CST 1970
        System.out.println(d2);

北京是东八区,需要在时间原点的基础上加上八个小时

常用方法

  • 通过set|get方法读写对象的毫秒值
        d2.setTime(1000l);
        System.out.println(d2); //Thu Jan 01 08:00:00 CST 1970
        System.out.println(d2.getTime()); //1000
  • 打印时间原点开始后一年的时间
        Date d = new Date(365 * 24 * 60 * 60 * 1000L);
        System.out.println(d); //Fri Jan 01 08:00:00 CST 1971
  • 定义两个Date对象,比较一下哪个时间在前,哪个时间在后
        Date d1 = new Date(10l);
        Date d2 = new Date(20l);
        System.out.println(d1.getTime() > d2.getTime() ? "d2":"d1" + "在前");

SimpleDateFormate

两个作用:

  1. 将Date转换为符合阅读习惯的格式
  2. 将字符串转换为Date类型对象
//构造方法
public SimpleDataFormat()   //使用默认格式构造一个SimpleDataFormat
public SimpleDataFormat(String pattern)  //使用指定格式构造一个SimpleDataFormat
public final String format(Date date)  //格式化  日期对象 -> 字符串
public Date parse(String source) //解析  字符串 -> 日期对象
  1. 将Date转换为符合阅读习惯的格式:
Date date = new Date();  
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
String formatDate = sdf.format(date);  
System.out.println(formatDate);
  1. 将字符串时间转化为Date对象
String str = "2023-11-11 11:11:11 111";  
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
  
Date parse = sdf.parse(str);  //注意抛出ParseException类型对象
System.out.println(parse); //Sat Nov 11 11:11:11 CST 2023
  • 练习:秒杀活动

开始时间:2023年11月11日 00:00:00 结束时间:2023年11月11日 00:10:00

A下单时间:2023年11月11日 00:01:00 B下单时间:2023年11月11日 00:11:00

String beginStr = "2023年11月11日 00:00:00";  
String endStr = "2023年11月11日 00:10:00";  
  
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");  
Date beginDate = sdf.parse(beginStr);  
Date endDate = sdf.parse(endStr);  
  
String aOrderStr = "2023年11月11日 00:01:00";  
String bOrderStr = "2023年11月11日 00:11:00";  
  
Date aOrderDate = sdf.parse(aOrderStr);  
Date bOrderDate = sdf.parse(bOrderStr);  
  
System.out.println("A" + (aOrderDate.getTime() >= beginDate.getTime() && aOrderDate.getTime() <= endDate.getTime() ? "成功" : "失败"));  
System.out.println("B" + (bOrderDate.getTime() >= beginDate.getTime() && bOrderDate.getTime() <= endDate.getTime() ? "成功" : "失败"));

JDK8 时间类API

JDK8的时间对象都是不可变的,修改了时间会产生新的对象

随着Java8的发布,Oracle同时推出了一套全新的时间API,算是填坑吧。旧版本时间API的坑相信大家也早有耳闻:

  • SimpleDateFormat是线程不安全的,多线程环境下共用同一个Formatter可能会抛异常
  • month被设计得非常奇葩,用011表示112月,所以如果要表示12月,你需要填写11,一不小心就掉坑里
  • 同时存在两个Date,一个是java.util下,另一个是java.sql下,API还不同,比较混乱
  • 时间计算非常麻烦
  • API设计太散了,散落在Date、Calendar、SimpleDateFormat三个类中,想用时找都找不到,干着急

当然,我个人觉得全新的时间API并不是特别重要(我们公司都是存秒数,而且TimeUtil已经很完备),只要了解即可,所以本文不会介绍得很深。

创建时间 now/of

首先要说明一点,新的时间API禁止了new的方式,全部通过工厂方式创建时间对象,其中最常用的就是now/of。

//旧的方式,new创建时间对象  
Date date = new Date();  
System.out.println("old api = " + date); //old api = Sat Nov 10 21:05:16 CST 3923

//新的方式,只能通过给定的方法获取  
LocalDate newDate = LocalDate.now();  
LocalTime newTime = LocalTime.now();  
LocalDateTime newDateTime = LocalDateTime.now();  
System.out.println("newDate = " + newDate);         //日期 newDate = 2023-11-10System.out.println("newTime = " + newTime);         //时间 newTime = 21:05:16.309733400System.out.println("newDateTime = " + newDateTime); //日期 + 时间 newDateTime = 2023-11-10T21:05:16.309733400  
//通过指定日期 + 时间获取LocalDateTime对象  
LocalDateTime combineDateTime = LocalDateTime.of(newDate, newTime);  
System.out.println("combineDateTime = " + combineDateTime); //combineDateTime = 2023-11-10T21:05:16.309733400  
  
//创建指定时间  
LocalDate customDate = LocalDate.of(2024, 11, 5);  
LocalTime customTime = LocalTime.of(16, 30, 0);  
LocalDateTime customDateTime = LocalDateTime.of(2024, 11, 6, 16, 30, 0);  
System.out.println("customDate = " + customDate);         //customDate = 2024-11-05  
System.out.println("customTime = " + customTime);         //customTime = 16:30  
System.out.println("customDateTime = " + customDateTime); //customDateTime = 2024-11-06T16:30

重点

  1. 分为LocalDate、LocalTime和LocalDateTime,now()创建当前时间,of()创建指定时间
  2. month类的BUG被修复了,为了避免和老版本产生歧义,为month单独提供了枚举(当然,你还是可以只填写数字)

疑问:老的Date和新的LocalXxx打印的格式不同,但表示的是同一个时间(后面介绍格式化)

增减时间 plus/minus

修改时间有两个含义:

  • 增加、减少时间
  • 替换时间

先介绍增减时间的方法。

要特别注意,新的时间类都是final修饰的,不可修改且线程安全(看注释),所以随便折腾。

如果你有过实际开发经验,就会明白老的时间API对于时间计算有多么不友好。举个实际开发经常会遇到的例子吧:

  • 获得当前时间和前一天此刻的时间,比如当前是2023-12-05 17:44:00,我还需要得到2023-12-04 17:44:00
  • 获取当天的零点和23:59:59
  • 一周前的今天、一个月前的今天、一年前的今天
public class NewDateApiTest {
    public static void main(String[] args) {
        Date today = new Date();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(today);
        calendar.add(Calendar.DAY_OF_MONTH, -1);
        Date yesterday = calendar.getTime();
        System.out.println("today:" + today);
        System.out.println("yesterday:" + yesterday);
    }
}

至于后面的两个需求,我根本写不出来,还是只能靠百度。比如我百度的结果是:

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

        long current = System.currentTimeMillis();
        long zeroT = current / 
					        (1000 * 3600 * 24) * (1000 * 3600 * 24) - TimeZone.getDefault().getRawOffset();
        long endT = zeroT + 24 * 60 * 60 * 1000 - 1;
        Date thisDayBegin = new Date(zeroT);
        Date thisDayEnd = new Date(endT);
        
    }
}

何必呢?

但这是我能力不行吗?NO,绝对是API设计得不好!API设计出来就是给普通开发人员用的,你搞得这么乱,这不是坑人嘛!很多人根本不知道去哪找对应的方法。

但新版时间API就不一样了,肯定是封装在LocalDate、LocalTime、LocalDateTime里的:

//获取时间参数的年、月、日  
System.out.println("获取时间LocalDateTime的年、月、日:");  
LocalDateTime localDateTime = LocalDateTime.of(2023,11,10,21,21,31);  
System.out.println("year:" + localDateTime.getYear());      //year:2023  
System.out.println("month:" + localDateTime.getMonth());    //month:NOVEMBER  
System.out.println("day:" + localDateTime.getDayOfMonth()); //day:10  
System.out.println("hour:" + localDateTime.getHour());      //hour:21  
System.out.println("minute:" + localDateTime.getMinute());  //minute:21  
System.out.println("second:" + localDateTime.getSecond());  //second:31  
  
//计算昨天的同一时刻(由于对象不可修改,返回的是新对象)  
System.out.println("计算前一天的当前时刻");  
LocalDateTime today = LocalDateTime.now();  
System.out.println("当前时刻:" + today);            //当前时刻:2023-11-10T21:29:12.555758600  
LocalDateTime yesterday1 = today.minusDays(1);  
LocalDateTime yesterday2 = today.minus(1, ChronoUnit.DAYS);  
LocalDateTime yesterday3 = today.plusDays(-1);  
LocalDateTime yesterday4 = today.plus(-1, ChronoUnit.DAYS);  
System.out.println("yesterday1 = " + yesterday1);  //yesterday1 = 2023-11-09T21:29:12.555758600  
System.out.println("yesterday2 = " + yesterday2);  //yesterday2 = 2023-11-09T21:29:12.555758600  
System.out.println("yesterday3 = " + yesterday3);  //yesterday3 = 2023-11-09T21:29:12.555758600  
System.out.println("yesterday4 = " + yesterday4);  //yesterday4 = 2023-11-09T21:29:12.555758600  
System.out.println("today.equals(yesterday1) = " + today.equals(yesterday1)); //today.equals(yesterday1) = false  
  
//计算当天的00点和24点,组合的强大之处  
LocalDate todayDate = LocalDate.now();  
LocalDateTime todayBegin = LocalDateTime.of(todayDate, LocalTime.MIN); //获取Time的最小值  
LocalDateTime todayEnd = LocalDateTime.of(todayDate, LocalTime.MAX);   //获取Time的最大值  
System.out.println("todayBegin = " + todayBegin);  
System.out.println("todayEnd = " + todayEnd);  
  
//计算一周,一个月,一年前的当前时刻  
System.out.println("计算一周、一个月、一年前的当前时刻:");  
LocalDateTime todayDateTime = LocalDateTime.now();  
LocalDateTime aWeekBefore = todayDateTime.minusWeeks(1);  
LocalDateTime aMonthBefore = todayDateTime.minusMonths(1);  
LocalDateTime aYearBefore = todayDateTime.minusYears(1);  
  
LocalDateTime oneWeekAgo = todayDateTime.minus(1, ChronoUnit.WEEKS);  
LocalDateTime oneMonthAgo = todayDateTime.minus(1, ChronoUnit.MONTHS);  
LocalDateTime oneYearAgo = todayDateTime.minus(1, ChronoUnit.YEARS);  
System.out.println("oneWeekAgo" + oneWeekAgo);  
System.out.println("oneMonthAgo" + oneMonthAgo);  
System.out.println("oneYearAgo" + oneYearAgo + "\n");  
  
System.out.println("aWeekBefore = " + aWeekBefore);   //aWeekBefore  = 2023-11-03 T21:41:37.038485200  
System.out.println("aMonthBefore = " + aMonthBefore); //aMonthBefore = 2023-10-10 T21:41:37.038485200  
System.out.println("aYearBefore = " + aYearBefore);   //aYearBefore  = 2022-11-10 T21:41:37.038485200

ChronoUnit是JDK提供的枚举,放心用就是了。plus/minus大概就这么个用法,上面展示的是最最通用的plus/minus(long amoutToAdd, TemporalUnit unit),数字+单位,随机应变。虽然plus配合负数时效果等同于minus,但如果要减去时间,还是建议使用minus。

你也可以选择以下方法进行时间的增减:

但这些都可以用plus/minus(long amoutToAdd, TemporalUnit unit)代替:

修改时间:with

从广义上来说,修改时间和增减时间是一样的,但稍微有点区别。我们通过几个案例体会一下即可:

LocalDateTime localDateTime = LocalDateTime.now();  
System.out.println("localDateTime = " + localDateTime);      //localDateTime = 2023-11-10T21:53:58.701544  
  
//将day修改为6  
LocalDateTime modifiedDateTime = localDateTime.with(ChronoField.DAY_OF_MONTH, 6);  
System.out.println("modifiedDateTime = " + modifiedDateTime); //modifiedDateTime = 2023-11-06T21:53:58.701544

注意一下,之前我们plus时用的是ChronoUnit,表示增幅单位,而这里ChronoField则是指定修改的字段。

同样的,上面演示的也是最最通用的API,大家也可以用以下方法:

也就是说,直接在方法层面已经指定了要修改的字段。

比较时间:isAfter/isBefore/isEqual

LocalDateTime today = LocalDateTime.now();  
LocalDateTime afterOneSecond = today.plusSeconds(1);  
  
boolean isAfter = afterOneSecond.isAfter(today);  
System.out.println("isAfter = " + isAfter); //isAfter = true

时区:zone

大家之前可能应该已经多多少少见过zone,那么什么是zone,为什么需要它呢?

由于地球是圆的,并且自西向东自转,美国的黑夜是我们的白昼,我们的早上8点是地球另一面的晚上8点。当一个国际友人问你现在几点钟时,你不能回答“现在是早上8点”,而应该说“现在是北京时间早点8点”或者“现在是美国时间晚上8点”

上面介绍的LocalDateTime等都是不包含时区信息的,但我们可以将它们转为包含时区信息的对象:

//当地时间  
LocalDateTime now = LocalDateTime.now();  
System.out.println("localDateTime = " + now); 
//localDateTime = 2023-11-10T22:09:20.334836600  
  
//获取时区ID,默认是本国时区  
ZoneId zoneId = ZoneId.systemDefault();  
//为LocalDateTime补充时区信息  
ZonedDateTime beiJingDateTime = now.atZone(zoneId);  
System.out.println("beiJingDateTime = " + beiJingDateTime); 
//beiJingDateTime = 2023-11-10T22:09:20.334836600+08:00[Asia/Shanghai]

这里有个容易犯错的点,需要和大家说明一下:LocalDateTime转为ZonedDateTime时间不会变,仅仅是丰富了时区信息而已。

LocalDateTime默认使用本地时区

比如:

//当地时间  
LocalDateTime now = LocalDateTime.now();  
System.out.println("localDateTime = " + now);              
//localDateTime = 2023-11-10T22:11:52.465027700  
  
//时区,id形式  
ZoneId zoneId = ZoneId.of("Asia/Tokyo");  
ZonedDateTime tokyoDateTime = now.atZone(zoneId);  
System.out.println("tokyoDateTime = " + tokyoDateTime);    
//tokyoDateTime = 2023-11-10T22:11:52.465027700+09:00[Asia/Tokyo]

时间是一样的,只是多了时区

那么,怎样转换时区时间呢?

用withZoneSameInstant():保持与时区同一时间

//当地时间  
LocalDateTime now = LocalDateTime.now();  
System.out.println("localDateTime = " + now);     
//localDateTime = 2023-11-10T22:17:02.427245100  
  
//时区,id形式  
ZoneId zoneId = ZoneId.of("Asia/Shanghai");  
  
//为LocalDateTime补充时区信息  
ZonedDateTime shangHaiDateTime = now.atZone(zoneId);  
System.out.println("上海时间:" + shangHaiDateTime);  
//上海时间:2023-11-10 T22:17:02.427245100+08:00[Asia/Shanghai]  
  
//当前上海时间对应东京时间是多少呢?  
ZonedDateTime tokyoDateTime = shangHaiDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));  
System.out.println("东京时间 = " + tokyoDateTime);  
//东京时间:2023-11-10 T23:17:02.427245100+09:00[Asia/Tokyo]

现在明白为什么旧的时间API叫Date,而新的时间API叫LocalXxx了吗?因为Date不带时区,只是一个普普通通的时间概念

比如你的生日是1998-12-11 12:00:00,没有时区。当你需要带上时区,可以转为ZonedDateTime。

那么我怎么知道北京时间是”Asia/Shanghai”,东京时间是”Asia/Tokyo”呀?百度。

反正我找了半天也没找到枚举。但是JDK提供了全部的ZoneId(没啥卵用):

public class NewDateApiTest {
    public static void main(String[] args) {
        Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(System.out::println);
    }
}

最后,ZonedDateTime转回LocalDateTime:

//当地时间  
LocalDateTime now = LocalDateTime.now();  
System.out.println("localDateTime = " + now);  
//localDateTime = 2023-11-10T22:20:27.834907100  
  
//时区,id形式  
ZoneId zoneId = ZoneId.of("Asia/Shanghai");  
  
//为LocalDateTime补充时区信息  
ZonedDateTime shangHaiDateTime = now.atZone(zoneId);  
System.out.println("上海时间:" + shangHaiDateTime);  
//上海时间:      2023-11-10 T22:20:27.834907100+08:00[Asia/Shanghai]  
  
LocalDateTime localDateTime = shangHaiDateTime.toLocalDateTime();  
System.out.println("localDateTime = " + localDateTime);  
//localDateTime = 2023-11-10 T22:20:27.834907100

toLocalDateTime()即可。

LocalDateTime与Date互转

媒介是Instant(格林尼治时间)

LocalDateTime转Date

//将LocalDateTime转为ZonedDateTime,然后调用toInstant()  
LocalDateTime now = LocalDateTime.now();//东八区时间  
ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault());  
  
//Instant是代表GMT,比东八区晚8小时  
Instant instant = zonedDateTime.toInstant();  
System.out.println("zonedDateTime = " + zonedDateTime);   
//东八区时间 zonedDateTime = 2023-11-10T23:37:02.005434100+08:00[Asia/Shanghai]System.out.println("instant = " + instant);   
//GMT时间 instant = 2023-11-10T15:37:02.005434100Z  
//转为Date  
Date date = Date.from(instant);//东八区时间  
System.out.println("date = " + date);  
//date = Fri Nov 10 23:37:02 CST 2023

Date转LocalDateTime

Date date = new Date();  
//转为GMT  
Instant instant = date.toInstant();  
System.out.println("date = " + date);  
//date = Fri Nov 10 23:55:41 CST 2023  
System.out.println("instant = " + instant);  
//instant = 2023-11-10T 15:55:41 .366Z  
  
//不带时区:LocalDateTime.of(),带时区LocalDateTime.ofInstant()  
LocalDateTime localDateTimeWithZone = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());  
System.out.println("localDateTimeWithZone = " + localDateTimeWithZone);  
//localDateTimeWithZone = 2023-11-10 T23:55:41 .366

转成秒也非常常用:

Date date = new Date();  
System.out.println(date.getTime() / 1000); //1699631904  
  
LocalDateTime now = LocalDateTime.now();  
  
long result = now.toEpochSecond(ZoneOffset.ofHours(8));  
System.out.println(result); //1699631904  
  
LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(result, 0, ZoneOffset.ofHours(8));  
System.out.println(localDateTime);

Instant 特定点时间戳对象

获取lnstant的对象可以拿到此刻的时间,该时间由两部分组成:从1970-01-01 00:00:00 开始走到此刻的总秒数 + 不够1秒的纳秒数

static Instant now()      //获取当前时间的Instant对象(标准时间不带时区)
static Instant ofXxx(long epochMilli) //根据 秒/毫秒/纳秒 获取Instant对象
    
/*非静态方法*/
ZonedDateTime atZone(ZoneId zone)  //指定时区
boolean isXxx(Instant otherInstant)  //判断相关的方法
Instant minusXxx(long millisToSubtract) //减少时间相关的方法
Instant plusXxx(long millisToSubtract)  //增加时间相关的方法

格式化

public class NewDateApiTest {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("格式化前:" + now);
        String format = now.format(DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("默认格式:" + format);
        String other = now.format(DateTimeFormatter.BASIC_ISO_DATE);
        System.out.println("其他格式:" + other);
    }
}

上面的代码只想透露3点信息:

  • 直接打印LocalDateTime默认是DateTimeFormatter.ISO_DATE_TIME
  • DateTimeFormatter已经提供了一些格式
  • 新版时间的格式化方法还是在自身,只不过需要传入DateTimeFormatter指定格式。旧版的格式化方法定义在SimpleDateFormat类中,传入时间+格式

但一般我们都需要自定义格式,怎么做?

先观察默认的格式化是怎么做的:

DateTimeFormatter.ISO_DATE_TIME其实返回的是DateTimeFormatter对象。

public class NewDateApiTest {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("格式化前:" + now);
   		String format = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
        System.out.println("格式化后:" + format);
    }
}

为什么可以传入DateTimeFormatter.ofPattern()呢?不是需要对象吗?

和之前学习Stream API时遇到的一样,某些方法的返回值其实也是自身类型:

最后了解一下反格式化:

public class NewDateApiTest {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("格式化前:" + now);
        String format = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
        System.out.println("格式化后:" + format);

        LocalDateTime parse = LocalDateTime.parse(format, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
        System.out.println("反格式化:" + parse);
    }
}

简而言之,一个format(),一个parse(),都要传入DateTimeFormatter对象,可以自定义也可以使用默认的。

如果你怕写错”yyyy-MM-dd HH:mm:ss.SSS”,可以自定义一个常量类,或者使用第三方定义的。

public final class DatePattern {

    public static final String YYYY_MM_DD_HH_MM_SS_SSS = "yyyy-MM-dd HH:mm:ss.sss";
    public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
    public static final String YYYY_MM_DD_WITH_SLASH = "yyyy/MM/dd";
    public static final String YYYY_MM_DD_WITH_STRIKE = "yyyy-MM-dd";
    // ...

}

实际开发案例

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping
    public UserPojo addUser(@RequestBody UserPojo userPojo) {
        userPojo.setBirthday(userPojo.getBirthday().plusDays(1));
        return userPojo;
    }

}

@Data
public class UserPojo {

    private String name;
    private LocalDateTime birthday;

}

返回值是:

ThreadLocal+SimpleDateFormat

上面提到SimpleDateFormat在多线程环境下可能出现问题,但这有个条件:

多线程环境共用一个SimpleDateFormat对象的parse()方法

针对这个问题,其实有多种解决策略:

  • 将SimpleDateFormat作为局部变量
  • 使用ThreadLocal(本质和上面一样)
  • 加锁

这里演示前两种,大家可以复制下来运行看看。

public final class DateUtil {

    private DateUtil() {
    }

    private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";

    private static final ThreadLocal<SimpleDateFormat> threadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat(DEFAULT_DATE_PATTERN));

    public static SimpleDateFormat getFormatter(String pattern) {
        SimpleDateFormat simpleDateFormat = threadLocal.get();
        simpleDateFormat.applyPattern(getPattern(pattern));
        return simpleDateFormat;
    }

    public static SimpleDateFormat getFormatter() {
        return threadLocal.get();
    }

    private static String getPattern(String pattern) {
        if (pattern != null && !"".equals(pattern)) {
            return pattern;
        }
        return DEFAULT_DATE_PATTERN;
    }


    /**
     * 测试案例,分别测试
     * 1.多线程共用一个SimpleDateFormat进行parse,会抛异常
     * 2.多线程内各自new SimpleDateFormat,不会抛异常
     * 3.引入ThreadLocal
     *
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
//        testException();
//        testFormatterWithNew();
//        testFormatterWithTL();
    }


    private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private static void testException() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Date parse = formatter.parse("2020-12-05 16:40:00");
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    private static void testFormatterWithNew() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100000);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            new Thread(() -> {
                try {
                    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    Date parse = formatter.parse("2020-12-05 16:40:00");
                    countDownLatch.countDown();
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start) + "毫秒");
    }

    private static void testFormatterWithTL() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100000);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            new Thread(() -> {
                try {
                    Date parse = DateUtil.getFormatter().parse("2020-12-05 16:40:00");
                    countDownLatch.countDown();
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start) + "毫秒");
    }
}

总结

本地时间类

  • LocalDate:年、月、日
  • LocalTime:时、分、秒、纳秒
  • LocalDateTime:年、月、日、时、分、秒、纳秒

获取方式:

  • public static Xxxx now(): 获取系统当前时间对应的该对象
LocaDate ld = LocalDate.now();
LocalTime lt = LocalTime.now();
LocalDateTime ldt = LocalDateTime.now();
  • public static Xxxx of(…):获取指定时间的对象
LocalDate localDate1 = LocalDate.of(2099 , 11,11);
LocalTime localTime1 = LocalTime.of(9,  8, 59);
LocalDateTime localDateTime1 = LocalDateTime.of(2025, 11, 16, 14, 30, 01);

常用API:

  • LocalDate类

//注意getDayOfWeek方法
LocalDateTime now = LocalDateTime.now();  
DayOfWeek dayOfWeek = now.getDayOfWeek();  
System.out.println(dayOfWeek.toString()); //SATURDAY  
  
//DayOfWeek是枚举类型,输出的时候默认输出name  
//如果想输出数字星期几,可以使用getValue方法:  
System.out.println(dayOfWeek.getValue());//6 return ordinal() + 1;  Monday是开始的
  • LocalTime类:

  • LocalDateTime:

  • 互相转换

练习:判断今天是否是某人的生日
LocalDate aBirthDay = LocalDate.of(2002, 11, 11);  
LocalDate bBirthDay = LocalDate.of(2001, 10, 26);  
//使用MonthDay更优雅
MonthDay todayMonthDay = MonthDay.of(LocalDate.now().getMonth(),LocalDate.now().getDayOfMonth());  
System.out.println("a的生日? " 
				   + todayMonthDay.equals(MonthDay.of(aBirthDay.getMonth(), aBirthDay.getDayOfMonth())));  
System.out.println("b的生日? " 
				   + todayMonthDay.equals(MonthDay.of(bBirthDay.getMonth(),bBirthDay.getDayOfMonth())));

时区时间类

  • ZoneId相关方法:
//ZoneId相关方法:
static Set<String> getAvailableZoneIds() // 获取Java中支持的所有时区
static ZoneId systemDefault() //获取系统默认时区
static ZoneId of(String zoneId) // 获取一个指定时区,获取时区就可以获取真实时间
  • ZoneDateTime相关方法:

LocalDateTime是不包含时区信息(或是包含默认时区信息)的,但是我们可以将它们转为包含时区信息的对象:

LocalDateTime now = LocalDateTime.now();  
//为LocalDateTime添加本地时区信息转换为ZonedDateTime  
ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault());

但是注意,LocalDateTime转换为ZonedDateTime时间不会变,仅仅是丰富了时区信息而已

如果想显示当前本地时间对应的东京时间呢?使用withZoneSameInstant,与时区保持统一时刻。

LocalDateTime now = LocalDateTime.now();  
//为LocalDateTime添加本地时区信息转换为ZonedDateTime  
ZonedDateTime defaultZonedDateTime = now.atZone(ZoneId.systemDefault());  
  
//显示defaultZonedDateTime对应的Asia/Tokyo时区时间  
ZonedDateTime tokyoDateTime = defaultZonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));  
System.out.println(tokyoDateTime);
  • ZonedDateTime转为LocalDateTime:
LocalDateTime now = LocalDateTime.now();  
//为LocalDateTime添加本地时区信息转换为ZonedDateTime  
ZonedDateTime defaultZonedDateTime = now.atZone(ZoneId.systemDefault());  
  
//显示defaultZonedDateTime对应的Asia/Tokyo时区时间  
ZonedDateTime tokyoDateTime = defaultZonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));  
System.out.println(tokyoDateTime);  
//2023-11-11T20:30:20.092344200+09:00[Asia/Tokyo]  
  
LocalDateTime localDateTime = tokyoDateTime.toLocalDateTime();  
System.out.println(localDateTime);//2023-11-11T20:30:20.092344200

注意,转回LocalDateTime是保持ZonedDateTime记录的时间。

练习:跨国数据库记录订单信息

有以下订单信息:

public class Order{
	private LocalDateTime orderTime;
}

现在美国网站和中国网站同时有用户进行购买,但是不能按照两地的当地时间记录Order的orderTime,就需要获取到UTC的标准时间,按照标准时间记录到服务器中:

ZonedDateTime utcNow = ZonedDateTime.now(Clock.systemUTC());
LocalDateTime fromUtcNow = utcNow.toLocalDateTime();
order.setOrderTime(fromUtcNow);

这样在美国或中国显示购买时间,就可以加上对应的偏移量。

格式化类

  • 格式化LocalDateTime:
LocalDateTime now = LocalDateTime.now();  
System.out.println("未格式化的时间 = " + now);  
//未格式化的时间 = 2023-11-11T17:55:16.630117600  
//一、格式化时间  
//1. 第一种格式化方案,使用DateTimeFormatter进行format  
DateTimeFormatter dtf1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS a");  
String formatTimeStr = dtf1.format(now);  
System.out.println("使用DateTimeFormat进行format = " + formatTimeStr);  
//使用DateTimeFormat进行format = 2023-11-11 17:55:16 630 下午  
  
//2. 第二种方案,使用LocalDateTime进行format  
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS a");  
String formatStr = now.format(dtf2);  
System.out.println("使用LocalDateTime进行format = " + formatStr);  
//使用LocalDateTime进行format = 2023-11-11 17:55:16 630 下午  
  

总结:

  1. 使用DateTimeFormatter进行格式化,与SimpleDateFormate相同
  2. 使用LocalDateTime进行格式化,新版时间API可以通过LocalDateTime自身进行format,因为DateTimeFormatter中提供了很多常量形式的时间格式化方式,例如BASIC_ISO_DATE和ISO_DATE_TIME,使用这两种方式就可以避免创建DateTimeFormatter对象。
        LocalDateTime now = LocalDateTime.now();
        System.out.println("格式化前:" + now);
        String format = now.format(DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("默认格式:" + format);
        String other = now.format(DateTimeFormatter.BASIC_ISO_DATE);
        System.out.println("其他格式:" + other);
  • 将字符串转换为LocalDateTime对象:
//二、解析时间  
String dataStr = "2023-11-11 11:11:11 111";  
DateTimeFormatter dtf3 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS");  
LocalDateTime parseTime = LocalDateTime.parse(dataStr, dtf3);  
System.out.println("parseTime = " + parseTime);

与原版的API不同的是,新版进行转换时是从LocalDateTime中进行parse的。

Instant 时间戳

获取lnstant的对象可以拿到此刻的时间,该时间由两部分组成:从1970-01-01 00:00:00 开始走到此刻的总秒数 + 不够1秒的纳秒数

工具类

Period

LocalDate localDate1 = LocalDate.of(2022, 11, 20);  
LocalDate localDate2 = LocalDate.of(2023, 11, 30);  
Period period = Period.between(localDate1, localDate2);  
System.out.println(period.getYears()); //1  
System.out.println(period.getMonths()); //0  
System.out.println(period.getDays()); //10

可以看到,Period只能计算LocalDate对象相差的年、月、天数,并且年月天都是直接加减,独立计算的。

Duration

Duration可以记录LocalTime、LocalDateTime对象相差的天、小时、分、秒、纳秒。

LocalDateTime localDateTime1 = LocalDateTime.of(2022, 10, 10, 10, 10, 10);  
LocalDateTime localDateTime2 = LocalDateTime.of(2023, 11, 11, 11, 11, 11);  
  
Duration duration = Duration.between(localDateTime1, localDateTime2);  
System.out.println(duration.toDays()); //397  
System.out.println(duration.toHours()); //9529  
System.out.println(duration.toMinutes()); //571741

可以看到,计算出的差值并不是独立的,而是将年月日作为整体计算相差几天。

假设有需求:设计一个高考倒计时,显示距离高考的天数:时:分:秒

使用以上两种方式都是不合适的,Period不能计算小时,可以使用Duration的toHoursPart()方法:

LocalDateTime localDateTime1 = LocalDateTime.of(2023, 10, 10, 10, 10, 10);  
LocalDateTime localDateTime2 = LocalDateTime.of(2024, 6, 7, 9, 00, 00);  
  
Duration duration = Duration.between(localDateTime1, localDateTime2);  
System.out.println(duration.toDays()); //240  
System.out.println(duration.toHoursPart()); //22  
System.out.println(duration.toMinutesPart()); //49

这样就可以将时、分单独的隔离计算。

ChronoUnit
 // 当前时间
        LocalDateTime today = LocalDateTime.now();
        System.out.println(today); //2023-03-24T10:14:00.901718500
        // 生日时间
        LocalDateTime birthDate = LocalDateTime.of(2000, 1, 1,0, 0, 0);
        System.out.println(birthDate);//2000-01-01T00:00

        System.out.println("相差的年数:" + ChronoUnit.YEARS.between(birthDate, today)); //相差的年数:23
        System.out.println("相差的月数:" + ChronoUnit.MONTHS.between(birthDate, today));//相差的月数:278
        System.out.println("相差的周数:" + ChronoUnit.WEEKS.between(birthDate, today));//相差的周数:1211
        System.out.println("相差的天数:" + ChronoUnit.DAYS.between(birthDate, today));//相差的天数:8483
        System.out.println("相差的时数:" + ChronoUnit.HOURS.between(birthDate, today));//相差的时数:203602
        System.out.println("相差的分数:" + ChronoUnit.MINUTES.between(birthDate, today));//相差的分数:12216134
        System.out.println("相差的秒数:" + ChronoUnit.SECONDS.between(birthDate, today));//相差的秒数:732968040
        System.out.println("相差的毫秒数:" + ChronoUnit.MILLIS.between(birthDate, today));//相差的毫秒数:732968040901
        System.out.println("相差的微秒数:" + ChronoUnit.MICROS.between(birthDate, today));//相差的微秒数:732968040901718
        System.out.println("相差的纳秒数:" + ChronoUnit.NANOS.between(birthDate, today));//相差的纳秒数:732968040901718500
        System.out.println("相差的半天数:" + ChronoUnit.HALF_DAYS.between(birthDate, today));//相差的半天数:16966
        System.out.println("相差的十年数:" + ChronoUnit.DECADES.between(birthDate, today));//相差的十年数:2
        System.out.println("相差的世纪(百年)数:" + ChronoUnit.CENTURIES.between(birthDate, today));//相差的世纪(百年)数:0
        System.out.println("相差的千年数:" + ChronoUnit.MILLENNIA.between(birthDate, today));//相差的千年数:0
        System.out.println("相差的纪元数:" + ChronoUnit.ERAS.between(birthDate, today));//相差的纪元数:0

综合练习

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

要求:字符串中只能是数字不能有其他字符;最少一位,最多十位;0不能开头

public static int parseInt(String intStr){  
    int num = 0;  
    for (int i = 0; i < intStr.length(); i++) {  
        num *= 10;  
        int n = intStr.charAt(i) - '0';  
        num += n;  
    }  
    return num;  
}
  • 实现toBinaryString方法的效果,将字符串形式的数据转成字符串二进制

思路一:除基取余法

public static String toBinaryString(String numStr){  
    int num = parseInt(numStr);  
    StringBuilder builder = new StringBuilder();  
    while (num > 0){  
        builder.append(num % 2);  
        num /= 2;  
    }  
    return builder.toString();  
}

但是如果要求是32位二进制数据就不行了,需要进一步处理

public static String toBinaryString(String numStr){  
    int num = parseInt(numStr);  
    StringBuilder builder = new StringBuilder();  
    while (num > 0){  
        builder.append(num % 2);  
        num /= 2;  
    }  
    for (int i = builder.reverse().length(); i < 32; i++) {  
        builder.append("0");  
    }  
    return builder.reverse().toString();  
}

思路二:parstInt后计算二进制,int32位,1 << 31 – 0

public static String toBinaryString(String numStr){  
    int num = parseInt(numStr);  
    StringBuilder builder = new StringBuilder();  
    for (int i = 31; i >= 0; i--) {  
        builder.append((num & (1 << i)) == 0 ? "0" : "1");  
    }  
    System.out.println();  
    return builder.toString();  
}

注意:& 结果只能和0相比,不能和1相比

要求:字符串中只能是数字不能有其他字符;最少一位,最多十位;0不能开头

JDK8 时间API:

需求背景:某林业工人孙工,作息规律为上三天班,休息一天,经常不确定休息日是否是周末。为此,请你开发一个程序,当孙工输入年以及月,以日历方式显示对应月份的休息日,用中括号进行标记(可以查看以前的休息情况和将来的休息情况)。同时,统计出本月有几天休息,轮到周末休息有几天。(注:首次休息日是2020年2月1日)

需求描述

1.将输入的年份和月份传入LocalDate类,找到该月份的最大天数

2.计算输入月份月末-首次休息日的间隔天数

3.计算间隔天数中的休息日并放入集合中

4.在间隔天的休息日中找到该查询月份的休息日并放入集合中

5.计算该月休息日的天数

LocalDate firstDate = LocalDate.of(2020, 2, 1);  
System.out.println("输入年份-月份:");  
String[] split = new Scanner(System.in).next().split("-");  
LocalDate thisDate = LocalDate.of(Integer.parseInt(split[0]), Integer.parseInt(split[1]), 1);  
int maxDayOfThisMonth = thisDate.lengthOfMonth();  //注意获取某个月的最大天数
LocalDate localDate = LocalDate.of(thisDate.getYear(), thisDate.getMonth(), maxDayOfThisMonth);  
  
System.out.println("间隔天数:" + ChronoUnit.DAYS.between(firstDate, localDate));  
  
ArrayList<LocalDate> restDays = new ArrayList<>();  
for (int i = 0; i <= ChronoUnit.DAYS.between(firstDate, localDate); i++) {  
    if (i % 4 == 0){  
        restDays.add(firstDate.plusDays(i));  
    }  
}  
System.out.println("间隔天数的所有休息日:" + restDays);  
  
ArrayList<LocalDate> restDayInThisMonth = new ArrayList<>();  
for (LocalDate restDay : restDays) {  
    if (restDay.getMonthValue() == Integer.parseInt(split[1])){  
        restDayInThisMonth.add(restDay);  
    }  
}  
System.out.println("本月所有休息日:" + restDayInThisMonth);  
System.out.println("本月休息日有" + restDayInThisMonth.size() + "天");  
  
ArrayList<String> sunDayList = new ArrayList<>();  
for (LocalDate restDay : restDays) {  
    if (restDay.getDayOfWeek().equals(DayOfWeek.SUNDAY)){  
        sunDayList.add("[" + restDay + "]");  
    }else {  
        sunDayList.add(restDay.toString());  
    }  
}  
System.out.println(sunDayList);

BigInteger

上限非常大,理论上最大到42亿的21亿次方

public BigInteger(int num, Random rnd) 		//获取随机大整数,范围:[0 ~ 2的num次方-1]
public BigInteger(String val) 				//获取指定的大整数
public BigInteger(String val, int radix) 	//指定进制的字符串转换为十进制整数
    										// 100 -> 4
    
下面这个不是构造,而是一个静态方法获取BigInteger对象
public static BigInteger valueOf(long val) 	//静态方法获取BigInteger的对象,内部有优化

BigInteger/BigDecimal类型对象一旦创建,内部记录的值不能发生改变

建议的获取方式:

  • 如果没有超过long类型的值,使用BigInteger.valueOf(long val) 获取
  • 超过long范围的值,再用BigInteger(String val) 获取

范围只能在long类型的范围之内,对常用的数字 -16 ~ +16创建了BigInteger对象,多次获取不会创建新对象

实际上是通过一个posConst存储整数,negConst存储负数,ZERO存储0

类加载时初始化1 ~ 16,-1 ~ -16

类加载时初始化0:

public static final BigInteger ZERO = new BigInteger(new int[0], 0);
BigInteger bi1 = BigInteger.valueOf(1);  
BigInteger bi11 = BigInteger.valueOf(1);  
System.out.println(bi1 == bi11); //true  
BigInteger bi2 = BigInteger.valueOf(100);  
BigInteger bi22 = BigInteger.valueOf(100);  
System.out.println(bi2 == bi22); //false

valueOf方法:

常用方法

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类型整数,超出范围数据有误
public  int longValue(BigInteger val)					//转为long类型整数,超出范围数据有误
  • public int intValue(BigInteger val)转为int类型整数,如果参数超过int类型最大值,返回值就会出错

底层存储方式

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

数组中最多能存储元素个数:21亿多

数组中每一位能表示的数字:42亿多

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

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

存储方式如图所示(从右到左32位划分为一组):

数组中:

signum:

  • -1 for negative
  • 0 for zero
  • 1 for positive

存入的是mag数组

数组的最大长度是int类型的最大值(2147483647 21亿),每一位都可以表示int类型的范围(-2147483648 – 2147483647 42亿)

BigDecimal

Java中提供的两种浮点数类型:

类型 占用字节数 总bit位数 小数部分bit位数
float 4个字节 32bit 23bit
double 8个字节 64bit 52bit

在使用float或者double类型的数据在进行数学运算的时候,很有可能会产生精度丢失问题。

计算机底层在进行运算的时候,使用的都是二进制数据;当我们在程序中写了一个十进制数据,在进行运算的时候,计算机会将这个十进制数据转换成二进制数据,然后再进行运算,计算完毕以后计算机会把运算的结果再转换成十进制数据给我们展示

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

  • 小数的精确计算
  • 表示很大的小数

构造方法:

public BigDecimal(double val)
public BigDecimal(String val)

也可以通过静态方法获取BigDecimal对象:

public static BigDecimal valueOf(double val)
  • 对于第一种构造方法(double val),结果可能是不可预计的
        BigDecimal bd1 = new BigDecimal(0.01);
        BigDecimal bd2 = new BigDecimal(0.09);
        System.out.println(bd1);//0.01000000000000000020816681711721685132943093776702880859375
        System.out.println(bd2);//0.0899999999999999966693309261245303787291049957275390625
  • 对于第二种构造方法(String val),结果是精确的
        BigDecimal bd1 = new BigDecimal("0.01");
        BigDecimal bd2 = new BigDecimal("0.09");
        System.out.println(bd1);//0.01
        System.out.println(bd2);//0.09

其实valueOf底层就是通过String入参的构造方法获取到BigDecimal对象的:

public static BigDecimal valueOf(double val) {    
    return new BigDecimal(Double.toString(val));  
}

对于基本类型转换为字符串的方法,建议使用包装类的toString方法

获取方式推荐:

  • 如果要表示的数字不大,没有超过double的取值范围,建议使用静态方法
  • 如果要表示的数字比较大,超出了double的取值范围,建议使用构造方法

valueOf()方法,对于long类型的参数:

如果在0-10之内,会在提前创建好的数组当中直接获取,如果超过这个范围再new返回

        BigDecimal bd1 = BigDecimal.valueOf(10);
        BigDecimal bd2 = BigDecimal.valueOf(10);
        System.out.println(bd1 == bd2); //true

        BigDecimal bd3 = BigDecimal.valueOf(10.0);
        BigDecimal bd4 = BigDecimal.valueOf(10);
        System.out.println(bd3 == bd4); //false

对于double类型参数,valueOf直接new对象返回

常用方法

/*
    public BigDecimal add(BigDecimal val) 加法
    public BigDecimal subtract(BigDecimal val) 减法
    public BigDecimal multiply(BigDecimal val) 乘法
    public BigDecimal divide(BigDecimal val) 除法
    public BigDecimal divide(BigDecimal val,精确几位,舍入模式)除法
*/
//1.加法
BigDecimal bd1 = BigDecimal.valueOf(10.);
BigDecimal bd2 = BigDecimal.valueOf(2.0);
BigDecimal bd3 = bd1.add(bd2);
System.out.println(bd3);

//2.减法
BigDecimal bd4 = bd1.subtract(bd2);
System.out.println(bd4);

//3.乘法
BigDecimal bd5 = bd1.multiply(bd2);
System.out.println(bd5);//20.00

//4.除法
BigDecimal bd6 = bd1.divide(bd2, 2, RoundingMode.HALF_UP);
System.out.println(bd6);//3.33

RoundingMode.HALF_UP在JDK9之前是BigDecimal.ROUND_HALF_UP,Java认为舍入模式不应该只定义在BigDecimal类而应该定义在单独的类中

如果使用BigDecimal类型的数据进行divide(BigDecimal val)除法运算的时候,得到的结果是一个无限循环小数,那么就会报错:ArithmeticException。 如下代码所示:

    public static void main(String[] args) {
        // 创建两个BigDecimal对象
        BigDecimal b1 = new BigDecimal("1") ;
        BigDecimal b2 = new BigDecimal("3") ;
        // 调用方法进行b1和b2的除法运算,并且将计算结果在控制台进行输出
        System.out.println(b1.divide(b2));
}

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
	at java.base/java.math.BigDecimal.divide(BigDecimal.java:1716)
	at com.itheima.api.bigdecimal.demo02.BigDecimalDemo02.main(BigDecimalDemo02.java:14)

此时我们就需要使用到BigDecimal类中另外一个divide方法,如下所示:

BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

divisor:			除数对应的BigDecimal对象;
scale:				精确的位数;
roundingMode:		取舍模式;
取舍模式被封装到了RoundingMode这个枚举类中,在这个枚举类中定义了很多种取舍方式。最常见的取舍方式有如下几个:
UP(直接进1) , FLOOR(直接删除) , HALF_UP(4舍5入),我们可以通过如下格式直接访问这些取舍模式:枚举类名.变量名

演示取舍模式:

 // 演示取舍模式HALF_UP
    public static void method_03() {

        // 创建两个BigDecimal对象
        BigDecimal b1 = new BigDecimal("0.3") ;
        BigDecimal b2 = new BigDecimal("4") ;

        // 调用方法进行b1和b2的除法运算,并且将计算结果在控制台进行输出
        System.out.println(b1.divide(b2 , 2 , RoundingMode.HALF_UP)); //0.08

    }

    // 演示取舍模式FLOOR
    public static void method_02() {

        // 创建两个BigDecimal对象
        BigDecimal b1 = new BigDecimal("1") ;
        BigDecimal b2 = new BigDecimal("3") ;

        // 调用方法进行b1和b2的除法运算,并且将计算结果在控制台进行输出
        System.out.println(b1.divide(b2 , 2 , RoundingMode.FLOOR)); // 0.33

    }

    // 演示取舍模式UP
    public static void method_01() {

        // 创建两个BigDecimal对象
        BigDecimal b1 = new BigDecimal("1") ;
        BigDecimal b2 = new BigDecimal("3") ;

        // 调用方法进行b1和b2的除法运算,并且将计算结果在控制台进行输出
        System.out.println(b1.divide(b2 , 2 , RoundingMode.UP)); //0.34

    }

后期在进行两个数的除法运算的时候,经常使用的是可以设置取舍模式的divide方法。

存储原理

把数据看成字符串,遍历得到里面的每一个字符,把这些字符在ASCII码表上的值,都存储到byte数组中。

练习

double[] arr = {0.1,0.2,2.1,3.2,5.56,7.21};

计算它们的总值及平均值(四舍五入保留小数点后2位)

    public static void main(String[] args) {
        double[] arr = {0.1,0.2,2.1,3.2,5.56,7.21};
        System.out.println(sum(arr));
        System.out.println(getAverage(arr));
    }

    public static BigDecimal sum(double[] arr){
        Objects.requireNonNull(arr);
        BigDecimal sum = BigDecimal.valueOf(0);
        for (int i = 0; i < arr.length; i++) {
            sum = sum.add(BigDecimal.valueOf(arr[i]));

        }
        return sum;
    }

    public static BigDecimal getAverage(double[] arr){
        BigDecimal sum = sum(arr);
        return sum.divide(BigDecimal.valueOf(arr.length),2, RoundingMode.HALF_UP);
    }

正则表达式

  1. 校验字符串是否满足规则
  2. 在一段文本中查找满足要求的内容
  • 字符类 [](只匹配一个字符)
表达式 解释
[abc] 只能是a或b或c
[^abc] 除了a或b或c之外的任何字符
[a-zA-Z] a到z A到Z
[a-z&&[def]] a-z和def的交集,就是d,e,f
[a-z&&[^bc]] a-z与非bc的交集
[a-z&&[^m-p]] a到z和除了m-p的交集

如果写成了一个&,就只表示一个&符号,没有任何含义

  • 预定义字符
表达式 解释
. 任何字符
\d 一个数字:[0-9]
\D 非数字:[^0-9]
\s 一个空白字符:[\t\n\x0B\f\r]
\S 非空白字符:[^\s]
\w [a-zA-Z_0-9]
\W [^\w]一个非单词字符

预定义字符\d前面的就是普通的\,需要转义,如果没有转移时:String reg = "\d" 报错,是因为Java认为此时的\d是一个特殊字符,而java中的特殊字符没有\d 只有 \s 之类,所以会报错

转义字符:改变后面那个字符原本的含义

        register.setIcon(new ImageIcon("..\\puzzlegame\\image\\login\\注册按钮.png"));

如果只写一个 \ ,就带表对后面的普通字符进行转义,所以要对 \ 进行转义

  • 数量词
表达式 解释
X? X出现0次或1次
X* X出现0次或多次
X+ X出现1次或多次
X X出现正好n次
X X出现至少n次
X X至少出现n次但是不超过m次
  • 其他常用符号
表达式 解释
(?i) 忽略后续大小写
竖线 或者
() 分组

练习是否满足规则

// 1、字符类(只能匹配单个字符)  
System.out.println("a".matches("[abc]"));    // [abc]只能匹配a、b、c  
System.out.println("e".matches("[abcd]")); // false  
System.out.println("ab".matches("[abcd]")); // false , 只能匹配一个字符  
  
System.out.println("d".matches("[^abc]"));   // [^abc] 不能是abc  
System.out.println("a".matches("[^abc]"));  // false  
  
System.out.println("b".matches("[a-zA-Z]")); // [a-zA-Z] 只能是a-z A-Z的字符  
System.out.println("2".matches("[a-zA-Z]")); // false  
  
System.out.println("k".matches("[a-z&&[^bc]]")); // : a到z,除了b和c  
System.out.println("b".matches("[a-z&&[^bc]]")); // false  
  
System.out.println("ab".matches("[a-zA-Z0-9]")); // false 注意:以上带 [内容] 的规则都只能用于匹配单个字符  
  
// 2、预定义字符(只能匹配单个字符)  .  \d  \D   \s  \S  \w  \W  
System.out.println("徐".matches(".")); // .可以匹配任意字符  
System.out.println("2".matches(".")); // .可以匹配任意字符  
System.out.println("徐徐".matches(".")); // false  
  
// \d 代表数字[0-9]  
// Java中“\”是很特殊的,\n \t代表的是一个特殊字符。  
// 如果希望 \ 就是 \ , 必须转义。  
System.out.println("2".matches("\\d"));  
System.out.println("a".matches("\\d"));  // false  
System.out.println("23".matches("\\d"));  // false  
  
  
System.out.println(" ".matches("\\s"));   // \s: 代表一个空白字符  
System.out.println("a".matches("\\s")); // false  
  
System.out.println("a".matches("\\S"));  // \S: 代表一个非空白字符  
System.out.println(" ".matches("\\S")); // false  
  
System.out.println("a".matches("\\w"));  // \w: [a-zA-Z_0-9]  
System.out.println("_".matches("\\w")); // true  
System.out.println("徐".matches("\\w")); // false  
  
System.out.println("徐".matches("\\W"));  // [^\w]不能是a-zA-Z_0-9  
System.out.println("a".matches("\\W"));  // false  
  
System.out.println("23232".matches("\\d")); // false 注意:以上预定义字符都只能匹配单个字符。  
  
// 3、数量词: ?   *   +   {n}   {n, }  {n, m}System.out.println("a".matches("\\w?"));   // ? 代表0次或1次  
System.out.println("".matches("\\w?"));    // true  
System.out.println("abc".matches("\\w?")); // false  
  
System.out.println("abc12".matches("\\w*"));   // * 代表0次或多次  
System.out.println("".matches("\\w*"));        // true  
System.out.println("abc12张".matches("\\w*")); // false  
  
System.out.println("abc12".matches("\\w+"));   // + 代表1次或多次  
System.out.println("".matches("\\w+"));       // false  
System.out.println("abc12张".matches("\\w+")); // false  
  
System.out.println("a3c".matches("\\w{3}"));   // {3} 代表要正好是n次  
System.out.println("abcd".matches("\\w{3}"));  // false  
System.out.println("abcd".matches("\\w{3,}"));     // {3,} 代表是>=3次  
System.out.println("ab".matches("\\w{3,}"));     // false  
System.out.println("abcde徐".matches("\\w{3,}"));     // false  
System.out.println("abc232d".matches("\\w{3,9}"));     // {3, 9} 代表是  大于等于3次,小于等于9次  
  
// 4、其他几个常用的符号:(?i)忽略大小写 、 或:| 、  分组:()  
System.out.println("----------------------------------------------------");  
System.out.println("abc".matches("(?i)abc")); // true  
System.out.println("ABC".matches("(?i)abc")); // true  
System.out.println("aBc".matches("a((?i)b)c")); // true  
System.out.println("ABc".matches("a((?i)b)c")); // false  
  
// 需求1:要求要么是3个小写字母,要么是3个数字。  
System.out.println("123".matches("\\d{3}|[a-z]{3}"));  
System.out.println("abc".matches("\\d{3}|[a-z]{3}"));  
System.out.println("aAc".matches("\\d{3}|[a-z]{3}")); // false  
  
// 需求2:必须是”我爱“开头,中间可以是至少一个”编程“,最后至少是1个”666“  
System.out.println("我爱编程编程666666".matches("我爱(编程)+(666)+"));  
System.out.println("我爱编程编程6666666".matches("我爱(编程)+(666)+")); // false
  • 需求:

​ 请编写正则表达式验证用户输入的手机号码是否满足要求。

​ 请编写正则表达式验证用户输入的邮箱号是否满足要求。

​ 请编写正则表达式验证用户输入的电话号码是否满足要求。

​ 验证手机号码 13112345678 13712345667 13945679027 139456790271

​ 验证座机电话号码 020-2324242 02122442 027-42424 0712-3242434

​ 验证邮箱号码 zhangsan@itcast.cnn dlei0009@163.com dlei0009@pci.com.cn

public static boolean checkEmail(String email){  
    return email.matches("\\w{2,30}@\\w{2,10}(\\.\\w{2,10}){1,2}");  //匹配单个字符. 需要进行转义
    //   /. 变为特殊字符报错      //.变为单个.
}

爬取数据

有如下文本,请按照要求爬取数据。

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

要求:找出里面所有的JavaXX

  • java.util.regex.Pattern:正则表达式对象
  • java.util.regex.Matcher:文本匹配器,按照正则表达式的规则读取字符串,从头开始读取
String data = "来黑马程序员学习Java,\n" +  
        "电话:18512516758,18512508907\n" +  
        "或者联系邮箱: boniu@itcast.cn\n" +  
        "座机电话:01036517895,010-98951256\n" +  
        "andy邮箱:bozai@itcast.cn,\n" +  
        "邮箱2:dlei0009@163.com,\n" +  
        "热线电话:400-618-9090 ,400-618-4000,\n" +  
        "4006184000,4006189090\n";

// 1、创建一个匹配规则对象,封装正则表达式(爬取的规则)  
//String regex = "(手机号正则)|(邮箱正则)|(座机正则)|(热线正则)"
String regex = "(1[3-9]\\d{9})|((0[1-9]\\d{1,4})-?[1-9]\\d{4,9})|(\\w{2,30}@\\w{2,20}(\\.\\w{2,10}){1,2})" +  
        "|(400-?[1-9]\\d{2,5}-?[1-9]\\d{2,5})";

// 2、把内容和爬取规则建立联系,得到一个匹配器对象  
Matcher matcher = pattern.matcher(data);

// 3、开始使用匹配器对象,开始爬取内容  
while (matcher.find()){  
    System.out.println(matcher.group());  
}

记录的数据:0,4

group()方法底层调用了subString(int beginIdx,int endIdx)方法,不会包含endIdx索引的字符,所以上面的find()方法记录的结束索引+1

条件爬取

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

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

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

        //?相当于前面的数据,也就是Java,=后面是要拼接的数据,获取的还是前半部分
        String regex = "Java(?=8|11|17)";
        Pattern compile = Pattern.compile(regex);
        Matcher m = compile.matcher(str);
        while (m.find()){
            System.out.println(m.group());
        }
    }
    /*
    Java
    Java
    Java
    Java
    * */
  • 需求2:爬取版本号为8,11,17的Java文本。正确爬取结果为:Java8 Java11 Java17 Java17
    public static void main(String[] args){
        String str = " Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台。";

        //String regex = "Java(8|11|17)";
        String regex = "Java(?:8|11|17)";
        
        Pattern compile = Pattern.compile(regex);
        Matcher m = compile.matcher(str);
        while (m.find()){
            System.out.println(m.group());
        }
    }
    /*
    Java8
    Java11
    Java17
    Java17
    * */
  • 需求3:爬取除了版本号为8,11,17的Java文本。
	public static void main(String[] args){
    	String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,因为这两个是"+
"长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台。";

		String regex = "Java(?!8|11|17)";

        Pattern compile = Pattern.compile(regex);
        Matcher m = compile.matcher(str);
        while (m.find()){
            System.out.println(m.group());
        }
	}
	/*
	Java
	* */

分组匹配

每组是有组号的,也就是序号:

  1. 组号从1开始,连续不间断
  2. 以左括号为基准,最左边的是第一组,其次是第二组,以此类推
(\\d+)(\\d+)(\\d+)
^       ^     ^
|       |     |
1       2     3

(\\d+(\\d+))(\\d+)
^      ^   ^  ^
|      |   |  |
1      2   1  3

捕获分组:就是将这一组中的数据捕获出来再用一次

需求:爬取邮箱@前的用户名

String data = "来黑马程序员学习Java,\n" +  
        "电话:18512516758,18512508907\n" +  
        "或者联系邮箱: boniu@itcast.cn\n" +  
        "座机电话:01036517895,010-98951256\n" +  
        "andy邮箱:bozai@itcast.cn,\n" +  
        "邮箱2:dlei0009@163.com,\n" +  
        "热线电话:400-618-9090 ,400-618-4000,\n" +  
        "4006184000,4006189090\n";  
  
// 1、创建一个匹配规则对象,封装正则表达式(爬取的规则)  
String regex = "(\\w{2,30})@(\\w{2,20})(\\.\\w{2,10}){1,2}";  
Pattern pattern = Pattern.compile(regex);  
  
// 2、把内容和爬取规则建立联系,得到一个匹配器对象  
Matcher matcher = pattern.matcher(data);  
  
// 3、开始使用匹配器对象,开始爬取内容  
while (matcher.find()){  
    System.out.println(matcher.group(1)); // 默认提取邮箱正则表达式第1个括号匹配的内容  
    System.out.println(matcher.group(2)); // 默认提取邮箱正则表达式第2个括号匹配的内容  
}

在爬取数据时,group方法可以自动的获取分组,只需要传递分组编号即可。

  • 需求:判断一个字符串的开始字符(1个)与结束字符是否一致,例如:a123a,b456b,17891,&abc&

将开头字符视为一组,捕获出来在结尾时进行判断

String reg_1 = "(.).*\\1";  //开始字符是任意的,中间内容是任意的,**结束字符是将第一组的内容拿出来再进行比较**

\\组号:将第X组的内容拿出来再用一次

  • 需求:判断一个字符串的开始部分和结束部分是否一致(多个),例如:abc123abc,b456b,123789123,&!@abc&!@
String reg_2 = "(.+).+\\1"; //开始部分是任意字符,可以出现多次
  • 需求:判断一个字符串的开始部分和结束部分是否一致?部分内部的字符也需要一致

例如:aaa123aaa,bbb456bbb,111789111,&&abc&&

String reg_4 = "((.)\\2)*.+\\1"; //第一个字符看作一组,该组会紧接着出现*次,这个整体再作为一组捕获到结束部分

注意:只能将第一个字符的内容提取出来再比较

捕获分组

捕获分组:后续还要使用本组中的数据;

  • 正则内部使用:\\组号
  • 正则外部使用:$组号

练习:将“我要学学学编编编编编编编编编程程程程程程程程”替换为 “我要学编程”

        /** 
		 * (.)一组:.匹配任意字符的。  
		 * \\1 :为这个组声明一个组号:1号  
		 * +:声明必须是重复的字  
		 * $1可以去取到第1组代表的那个重复的字  
		 */
        String str = "我要学学学编编编编编编编编编程程程程程程程程";
        String s = str.replaceAll("(.)\\1*", "$1");
        System.out.println(s);

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

符号 含义 举例
(?:正则) 获取所有 (Java?:8|11|17)
(?=正则) 获取前半部分 (Java?=8|11|17)
(?!正则) 获取不是指定内容的前半部分 (Java?!8|11|17)

在之前的身份证号练习中:

String regex = "[1-9]\\d{16}(?:\\d|X|x)";

加上?:就不会占用组号了,注意 ?相当于前面所有的数据,也就是[1-9]\\d{16}

贪婪匹配

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

如果按照ab+的方式获取数据,可能得到的是ab,也可能得到的是abbbbbbb

  • 贪婪爬取:按照ab+的方式爬取ab,b尽可能多获取 abbbbbbbbbbbb
  • 非贪婪爬取:按照ab+的方式爬取ab,b尽可能少获取 ab

Java中默认的是贪婪爬取,如果在_数量词_后面加上 ?,就是非贪婪爬取

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

替换与切割

public String replaceAll(String regex,String newStr);//按照正则表达式的规则进行替换
public String[] split(String regex); //按照正则表达式的规则切割字符串

小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠

  • 要求1:把字符串中三个姓名之间的字母替换为vs
  • 要求2:把字符串中的三个姓名切割出来
        String s = "小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠";
        
        String vs = s.replaceAll("[\\w&&[^_]]+", "vs");
        System.out.println(vs);//小诗诗vs小丹丹vs小惠惠

replaceAll方法源码:

replaceAll()方法也会创建文本解析器对象,从头开始读取字符串中的内容,只要有满足的就用第二个参数进行替换

        String s = "小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠";

        String[] split = s.split("[\\w&&[^_]]+");
        System.out.println(Arrays.toString(split));
// 1、public String replaceAll(String regex , String newStr):按照正则表达式匹配的内容进行替换  
// 需求1:请把 古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴,中间的非中文字符替换成 “-”String s1 = "古力娜扎ai8888迪丽热巴99fafas9aa5566马尔扎哈fbbADFFfsfs42425卡尔扎巴";  
// 参数一:正则表达式,匹配内容的。 参数二:替换的内容  
String result = s1.replaceAll("\\w+", "-");  
System.out.println(result);

// 2、public String[] split(String regex):按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组。  
// 需求1:请把 古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴,中的人名获取出来。  
String[] names = s1.split("\\w+");  
System.out.println(Arrays.toString(names));
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。