引言
【因内容较多,将拆分为两篇博文,此为篇一】
本篇博文为 Java 的一些高级特性的常见概念及相关细节梳理,意在重学 Java 查漏补缺。
博文随时会进行更新,补充新的内容并修正错漏,该系列博文旨在帮助自己巩固扎实 Java 基础。
毕竟万丈高楼,基础为重,借此督促自己时常温习回顾。
多线程
概念
程序、进程、线程
程序(Program):为完成特定任务,使用某种编程语言编写的一组指令的集合。即指一段静态的代码,静态对象
进程(Process):指程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程 —— 生命周期
- 如:运行中的 Edge 浏览器、Chrome 浏览器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(Thread):进程可进一步细化为线程,是一个程序内部的一条执行路径
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间(它们从同一堆中分配对象,可以访问相同的变量和对象,使得线程间通信更便捷、高效。但多个线程操作共享的系统资源可能会带来安全隐患)
单核 CPU、多核 CPU
单核 CPU 中其实是一种假的多线程,因为在一个时间单元内,只能执行一个线程的任务
- 多个任务只能由一个 CPU 进行处理,将正在执行中的任务”挂起”之后再去执行其他的任务,如此轮转。因为 CPU 时间单元特别短,因此感觉不出任务的切换
- 多核 CPU 才能更好的发挥多线程的效率
- 一个 Java 应用程序其实至少有三个线程:main() 主线程,gc() 垃圾回收线程,异常处理线程(如果发生异常,会影响主线程)
并行与并发
- 并行:多个 CPU 同时执行多个任务(多个人同时做不同的事)
- 并发:一个 CPU(采用时间片轮转)同时执行多个任务(多个人做同一件事)
多线程的优点与使用场景
优点:
- 提高应用程序的响应
- 提高计算机系统 CPU 的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
使用场景:
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时
- 需要一些后台运行的程序时
线程的分类
Java 中的线程分为两类:一种是守护线程,一种是用户线程
- 它们几乎每方面都是相同的,唯一的区别是判断 JVM 何时离开
- 守护线程是用来服务用户线程的,通过在 start() 方法前调用 thread.setDaemon(true) 可以把一个用户线程变成一个守护线程
- Java 垃圾回收就是一个典型的守护线程
- 若 JVM 中都是守护线程,当前 JVM 将退出
线程的创建和使用
多线程的创建
创建线程方式一:继承于 Thread 类
- 创建一个继承于 Thread 类的子类
- 重写 Thread 类的 run() 方法
- 创建 Thread 类的子类的对象
- 通过此子类对象调用 start() 方法:
- 启动当前线程
- 调用当前线程的 run() 方法
注意:
- 不能通过直接调用 run() 的方式启动线程
- 不可以让已经 threadSubObj.start() 的线程再去执行启动线程( threadSubObj.start() )的操作。会报错:IllegalThreadStateException
创建线程方式二:实现 Runnable 接口
- 创建一个实现了 Runnable 接口的类
- 实现类去实现 Runnable 中的抽象方法:run() 方法
- 创建实现类的对象
- 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
- 通过这个 Thread 类的对象调用 start() 方法:
- 启动当前线程
- 调用当前线程的 run() 方法(调用了 Runnable 类型的 target 的 run() 方法)
创建线程的两种方式的比较:
- 开发中:优先选择实现 Runnable 接口的方式
- 实现的方式没有类的单继承的局限性
- 实现的方式更适合用来处理多个线程有共享数据的情况
- 两种实现方式的联系:( Thread 类也是 Runnable 的实现类 ) public class Thread implements Runnable
- 相同点:两种方式都需要重写 run() 方法,将线程要执行的逻辑声明在 run() 方法中
创建线程方式三:实现 Callable 接口 (JDK5.0新增)
import java.util.concurrent.Callable;
- 创建一个实现 Callable 接口的实现类
- 实现 call() 方法,将此线程需要执行的操作声明在 call() 方法中
- 创建 Callable 接口实现类的对象
- 将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 的对象
- 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start() 方法
- (可选)获取 Callable 中 call() 方法的返回值
get() 返回值即为 FutureTask 构造器参数 —— Callable 实现类重写的 call() 的返回值
与使用 Runnable 相比,Callable 功能更强,针对 Callable:
- 相比 run() 方法,call() 方法可以有返回值
- call() 方法可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable 支持泛型的返回值
- 需要借助 FutureTask 类,比如获取返回结构
Future 接口:
- 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
- FutureTask 是 Future 接口的唯一的实现类
- FutureTask 同时实现了 Runnable,Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
创建线程方式四:使用线程池 (JDK5.0新增)
开发中线程是经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。若提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
JDK5.0起提供了线程池相关 API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable
- <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
- void shutdown():关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用的固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
创建及使用:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
提供指定线程数量的线程池(ExecutorService service = Executors.newFixedThreadPool(nThreads: 10); )
执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象
- service.executer(Runnable runnable); 适用于 Runnable
- service.submit(Callable callable); 适用于 Callable
关闭连接池(service.shutdown(); )
线程池的优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
Thread 类的常见方法
- void start():启动线程,并执行对象的 run() 方法
- run():线程在被调度时执行的操作
- 通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中
- static Thread currentThread():返回当前线程。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
- 静态方法,返回执行当前代码的线程
- String getName():返回该线程的名称
- 获取当前线程的名称
- void setName(String name):设置该线程名称
- 设置当前线程的名称
- static void yield():线程让步
- 释放当前 CPU 的执行权
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
- join():当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
- 在线程 thread1 中调用线程 thread2 的 join(),此时线程 thread1 就进入阻塞状态,直到线程 thread2 完全执行完成后,线程 thread1 才结束阻塞状态
- 低优先级的线程也可以获得执行
- static void sleep(long millis):(指定时间:毫秒)
- 让当前线程”睡眠”指定的毫秒。在指定的毫秒时间内,当前线程是阻塞状态
- 令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重新排队
- 抛出 InterruptedException 异常(需要使用 try-catch)
- stop():强制线程生命期结束,不推荐使用(已过时)
- 当执行此方法时,强制结束当前线程
- boolean isAlive():判断线程是否还活着
线程的调度
调度策略:
- 时间片
- 抢占式:高优先级的线程抢占 CPU
Java 的调度方法:
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的优先级:
- 现成的优先级等级:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5 (默认优先级)
- 涉及的方法:
- getPriority():返回线程优先值
- setPriority(int newPriority):改变线程的优先级
- 说明:
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
- 高优先级的线程要抢占低优先级线程 CPU 的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后低优先级的线程才执行
线程的生命周期
JDK 中用 Thread.State 类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建(new):当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被 start() 后将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没有分配到 CPU 资源
- 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run() 方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时终止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地终止或出现异常导致结束
线程的同步
线程存在的问题:
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对数据的共享,会造成操作的不完整性,会破坏数据
Java 中通过同步机制来解决线程的安全问题
线程同步方式一:同步代码块
synchronized(同步监视器) { |
说明:
- 操作共享数据的代码,即为需要被同步的代码
共享数据:多个线程共同操作的变量(数据) - 同步监视器,俗称:”锁”。任何一个类的对象,都可以充当锁
要求:多个线程必须要共用同一把锁 - 补充:
- 在实现 Runnable 接口创建多线程的方式中,可以考虑使用 this 充当同步监视器
- 在继承于 Thread 类创建多线程的方式中,可以考虑使用当前类本身(ClassX.clss)充当同步监视器
线程同步方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,可以将此方法声明为同步的
关于同步方法的总结:
- 同步方法仍然涉及同步监视器,只是不需要显式的声明
- 非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
线程同步方式三:Lock(锁)
- 从 JDK5.0开始,Java 提供了更强大的线程同步机制 —— 通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当
- java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具
- 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
- ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁
使用方式:
import java.util.concurrent.locks.ReentrantLock; |
synchronized 与 Lock 的异同:
- 相同:二者都可以解决线程安全问题
- 不同:
- synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器
- Lock 需要手动的启动( lock() ),同时结束同步也需要手动实现( unlock() )
synchronized 与 Lock 的对比:
- Lock 是显式锁(手动开启和关闭锁),synchronized 是隐式锁,出了作用域自动释放
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)
线程同步的局限性:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低
线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
关于锁的小结
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步方法、同步代码块中遇到 break、return 终止了该方法、该代码块的继续执行
- 当前线程在同步方法、同步代码块中出现了未处理的 Error 或 Exception,导致异常结束
- 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
不会释放锁的操作:
- 线程执行同步方法或同步代码块时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(同步监视器)
- 应尽量避免使用 suspend() 和 resume() 来控制线程
线程的通信
例子:交替打印、生产者消费者
涉及到的三个方法:
- wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器
- notify():一旦执行此方法,就会唤醒被 wait() 的一个线程。如果有多个线程被 wait,就唤醒优先级高的线程
- notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程
说明:
- wait()、notify()、notifyAll() 这三个方法必须使用在同步代码块或同步方法中
- wait()、notify()、notifyAll() 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器
否则,会出现 IllegalMonitorStateException 异常 - wait()、notify()、notifyAll() 这三个方法定义在 java.lang.Object 类中
sleep() 和 wait() 的异同:
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
不同点:
- 两个方法声明的位置不同:
- Thread 类中声明 sleep()
- Object 类中声明 wait()
- 调用的要求不同:
- sleep() 可以在任何需要的场景下调用
- wait() 必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器(锁):
- 如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放、wait() 会释放
- 两个方法声明的位置不同:
常用类
字符串相关的类
String 类
String 类:代表字符串。Java 程序中的所有字符串字面值(如”str”)都为此类的实例实现
- String 实现了 Serializable 接口:表示字符串是支持序列化的
- String 内部定义了 final char[] value 用于存储字符串数据。String 代表不可变的字符序列(不可变性)
- 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
- 当对现有的字符串进行连接操作时,需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
- 当调用 String 的 replace() 方法修改指定字符或字符串时,需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
- 通过字面量的方式(区别于 new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
- 字符串常量池中不会存储相同内容的字符串
- 字符串是常量,用双引号引起来表示。值在创建之后不能更改
String 对象的创建:
- 通过字面量定义的方式:此时变量指向的数据声明在方法区中的字符串常量池中
- 通过 new + 构造器的方式:此时变量指向数据在堆空间中开辟空间以后对应的地址值
- 因此会有如下结果:
String s1 = “str”;String s2 = “str”;
String s3 = new String(“str”);String s4 = new String(“str”);
s1 == s2 结果为 true、s1 == s3 结果为 false、s3 == s4 结果为 false - 字符串常量存储在字符串常量池,目的是共享
- 字符串非常量对象存储在堆中
- String s = new String(“str”); 方式创建对象,在内存中创建了两个对象:
- 一个是堆空间中 new 结构
- 另一个是 char[] 对应的常量池中的数据:”str”
String 的拼接特性:
- 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量
- 只要其中一个是变量,结果就在堆中
- 如果拼接的结果调用 intern() 方法,返回值就在常量池中
// 通过字面量定义的方式 |
String 常用方法:
- int length():返回字符串的长度:return value.length
- char charAt(int index):返回某索引处的字符:return value[index]
- boolean isEmpty():判断是否是空字符串:return value.length == 0
- String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
- String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为小写
- String trim():返回字符串的副本,忽略前导空白和尾部空白
- boolean equals(Object obj):比较字符串的内容是否相同
- boolean equalsIgnoreCase(String anotherString):与 equals 方法类似,忽略大小写
- String concat(String str):将指定字符串连接到此字符串的结尾。等价于用 “+”
- int compareTo(String anotherString):比较两个字符串的大小
- String substring(int beginIndex):返回一个新的字符串,它是此字符串的从 beginIndex 开始截取到最后的一个子字符串
- String substring(int beginIndex, int endIndex):返回一个新的字符串,它是此字符串的从 beginIndex 开始截取到 endIndex(不包含)的一个子字符串
- boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
- boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
- boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
- boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
- int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
- int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
- int lastIndexOf(String str):返回指定子字符串在此字符串中最后出现处的索引
- int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后出现处的索引,从指定的索引开始反向搜索
indexOf 和 lastIndexOf 方法如果未找到都是返回 -1 - String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的
- String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串
- String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串
- String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串
- boolean matches(String regex):告知此字符串是否匹配给定的正则表达式
- String[] split(String regex):根据给定正则表达式的匹配拆分此字符串
- String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过 limit 个,如果超过了,剩下的全部拼接为一个元素
String 与其他类型之间的转换
字符串与基本数据类型、包装类之间的相互转换:
- 字符串 -> 基本数据类型、包装类:
- 调用包装类的静态方法
- 基本数据类型、包装类 -> 字符串:
- 调用 String 重载的 valueOf() 方法
字符串与字符数组之间的相互转换:
- 字符数组 -> 字符串:
- String 类的构造器 String(char[]):用字符数组中的全部字符创建字符串对象
- String(char[], int offset, int length):用部分字符创建字符串对象
- 字符串 -> 字符数组:
- public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法
- public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):将指定索引范围内的字符串存放到字符数组中的方法
字符串与字节数组之间的相互转换:
- 字节数组 -> 字符串:
- String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String
- String(byte[], int offset, int length):用指定的字节数组的一部分,即从数组的起始位置 offset 开始取 length 个字节构造一个字符串对象
- 字符串 -> 字节数组:
- public byte[] getBytes():使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中
- public byte[] getBytes(String charsetName):使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组
- 解码时要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码
StringBuffer 和 StringBuilder
String、StringBuffer、StringBuilder 三者的异同:
- String:不可变的字符序列:底层使用 char[] 存储
- StringBuffer:可变的字符序列:线程安全的,效率较低,底层使用 char[] 存储
- StringBuilder:可变的字符序列:jdk5.0 新增,线程不安全的,效率较高,底层使用 char[] 存储
StringBuffer 和 StringBuilder 的常用方法:
- StringBuffer append(参数列表):提供了一些 append() 方法,用于进行字符串拼接
- StringBuffer delete(int start, int end):删除指定位置的内容
- StringBuffer replace(int start, int end, String str):把[start, end) 位置(左闭右开,包含左不包含右)替换为 str
- StringBuffer insert(int offset, xxx):在指定位置插入 xxx
- StringBuffer reverse():把当前字符序列逆转
- 当 append 和 insert 时,如果原来 value 数组长度不够,可扩容
- 如上方法支持方法链操作(sB.append(“a”).append(“b”).append(“c”))
方法链原理:
|
JDK8 之前的日期时间 API
java.lang.System 类
System 类提供的 public static currentTimeMillis() 用来返回当前时间与 1970 年 1 月 1 日 0 时 0 分 0 秒之间以毫秒为单位的时间差(时间戳)
- 此方法适于计算时间差
计算世界时间的主要标准:
- UTC(Coordinated Universal Time)
- GMT(Greenwich Mean Time)
- CST(Central Standard Time)
java.util.Date 类
表示特定的瞬间,精确到毫秒
- 构造器:
- Date():使用无参构造器创建的对象可以获取本地当时时间
- Date(long date):创建指定毫秒数的 Date 对象
- 常用方法:
- getTime():返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数
- toString():把此 Date 对象转换为以下形式的String:dow mon dd hh:mm:ss zzz yyyy 其中:dow 是一周中的某一天(Sun、Mon、Tue、Wed、Thu、Fri、Sat),zzz 是时间标准
- 其他很多方法已过时
java.text.SimpleDateFormat 类
Date 类的 API 不易于国际化,大部分被废弃了,java.text.SimpleDateFormat 类是一个不与语言环境有关的方式来格式化和解析日期的具体类。它允许进行格式化:日期 -> 文本,解析:文本 -> 日期
格式化:
- SimpleDateFormat():默认的模式和语言环境创建对象
- public SimpleDateFornat(String pattern):该构造方法可以用参数 pattern 指定的格式创建一个对象,该对象调用:
- public String format(Date date):方法格式化事件对象 date
解析:
- public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期
- 解析要求字符串必须是符合 SimpleDateFormat 识别的格式(通过构造器参数体现)
java.util.Calendar 类(日历类)
Calendar 是一个抽象基类,主要用于完成日期字段之间相互操作的功能
获取 Calendar 实例的方法:
- 使用 Calendar.getInstance() 方法
- 调用它的子类 GregorianCalendar 的构造器
一个 Calendar 的实例是系统时间的抽象表示,通过 get(int field) 方法来取得想要的时间信息。比如 YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY、MINUTE、SECOND
- public void set(int field, int value):将指定 field 设置成新的值
- public void add(int field, int amount):将指定 field 增加 amount、减少可以使用负的 amount
- public final Date getTime():通过日历类获取 Date 对象
- public final void setTime(Date date):将 Date 的对象的时间设置成日历类的时间
- 注意:
- 获取月份时:一月是 0、二月是 1,以此类推
- 获取星期时:周日是1、周二是 2,以此类推
JDK8 中新的日期时间 API
Date 与 Calendar 面临的问题:
- 可变性:像日期和时间这样的类应该是不可变的
- 偏移性:Date 中的年份是从 1900 开始的,二月份都从 0 开始
- 格式化:格式化只对 Date 有用,Calendar 则不行
- 此外它们也不是线程安全的、不能处理闰秒等
- 闰秒:指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少 1 秒的调整
java.time
java.time:包含值对象的基础包
java.time 中包含了所有关于:本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类
LocalDate、LocalTime、LocalDateTime 它们的实例是不可变的对象,它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息
- LocalDate 代表 ISO 格式(yyy-MM-dd)的日期
- LocalTime 表示一个时间
- LocalDateTime 用来表示日期和时间
- 注:ISO-8601 日历系统是国际标准化组织制定的现代公民的日期和时间的表示法(公历)
关于 LocalDate、LocalTime、LocalDateTime 类创建对象及其对象的相关方法:
- now() / now(ZoneId zone):静态方法,根据当前时间创建对象 / 指定时区的对象
获取当前的日期、时间、日期+时间 - of():静态方法,根据指定日期/时间创建对象
设置指定的年、月、日、时、分、秒。没有偏移量 - getDayOfMonth() / getDayOfYear():获得月份天数(1-31) / 获得年份天数(1-366)
- getDayOfWeek():获得星期几(返回一个 DayOfWeek 枚举类)
- getMonth():获得月份(返回一个 Month 枚举类)
- getMonthValue() / getYear():获得月份(1-12) / 获得年份
- getHour() / getMinute() / getSecond():获得当前对象对应的小时、分钟、秒
- withDayOfMonth() / withDayOfYear() / withMonth() / withYear():将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
- plusDays() / plusWeeks() / plusMonths() / plusYears() / plusHours():向当前对象增加几天、几周、几个月、几年、几小时
- minusDays() / minusWeeks() / minusMonths() / minusYears() / minusHours():向当前对象减去几天、几周、几个月、几年、几小时
Instant(瞬时):时间线上的一个瞬时点
只是简单的表示自 1970 年 1 月 1 日 0 分 0 秒(UTC) 开始的秒数)
因为 java.time 包是基于纳秒计算的,所以 Instant 的精度可以达到纳秒级(1 ns = 10^(-9) s、1秒=1000毫秒=10^6微妙=10^9纳秒)
关于 Instant 类创建对象及其对象的相关方法:
- now():静态方法,返回默认 UTC 时区的 Instant 类的对象
- ofEpochMilli(long epochMilli):静态方法,返回在1970-01-01 00:00:00 基础上加上指定毫秒数之后的 Instant 类的对象
- atOffset(ZoneOffset offset):结合即时的偏移来创建一个 OffsetDateTime
- toEpochMilli():返回 1970-01-01 00:00:00 到当前时间的毫秒数,即为时间戳
java.time.format.DateTimeFormatter 类
该类提供了三种格式化方法:
预定义的标准格式
- ISO_LOCAL_DATE_TIME
- ISO_LOCAL_DATE
- ISO_LOCAL_TIME
本地化相关的格式:
ofLocalizedDate(FormatStyle.LONG):适用于LocalDate
- FormatStyle.FULL
- FormatStyle.LONG
- FormatStyle.MEDIUM
- FormatStyle.SHORT
ofLocalizedDateTime(FormatStyle.LONG):适用于 LocalDateTime
- FormatStyle.LONG
- FormatStyle.MEDIUM
- FormatStyle.SHORT
自定义的格式,如:ofPattern(“yyyy-MM-dd hh:mm:ss E”)
常用方法:
- ofPattern(String pattern):静态方法,返回一个指定字符串格式的 DateTimeFormater
- format(TemporalAccessor t):格式化一个日期、时间,返回字符串
- parse(CharSequence texr):将指定格式的字符序列解析为一个日期、时间
其他 API
- ZoneId:该类中包含了所有的时区信息,一个时区的 ID,如 Europe/Paris
- ZonedDateTime:一个在 ISO-8601 日历系统时区的日期时间,如 2020-11-22T16:31:22+01:00 Europe/Paris
- 其中每个时区都对应着 ID,地区 ID 都为 “{区域}/{城市}” 的格式,例如:Asia/Shanghai 等
- Clock:使用时区提供对当前即时、日期和时间的访问的时钟
- 持续时间:Duration,用于计算两个 “时间” 间隔
- 日期间隔:Period,用于计算两个 “日期” 间隔
- TemporalAdjuster:时间校正器。如:将日期调整到 “下一个工作日” 等操作
- TemporalAdjusters:该类通过以下静态方法提供了大量的常用 TemporalAdjuster 的实现:
- firstDayOfXxx()
- lastDayOfXxx()
- nextXxx()
与遗留日期处理类的转换
类 | 转换为(To)遗留类 | 升级为新类 |
---|---|---|
java.time.Instant 与 java.util.Date | Date.from(instant) | date.toinstant() |
java.time.Instant 与 java.sql.Timestamp | Timestamp.from(instant) | timestamp.toinstant() |
java.time.zonedDateTime 与 java.util.GregorianCalendar | GregorianCalendar.from(zonedDateTime) | cal.toZonedDateTime() |
java.time.LocalDate 与 java.sql.Date | Date.valueOf(localDate) | date.toLocalDate() |
java.time.LocalTime 与 java.sql.Time | Date.valueOf(localTime) | date.toLocalTime() |
java.time.LocalDateTime 与 java.sql.Timestamp | Timestamp.valueOf(localDateTime) | timestamp.toLocalDateTime() |
java.time.ZoneId 与 java.util.TimeZone | Timestamp.getTimeZone(id) | timeZone.toZonedId() |
java.time.format.DateTimeFormatter 与 java.text.DateFormat | formatter.toFormat() | 无 |
Java 比较器
在 Java 中经常会涉及到对象数组的排序问题,涉及到对象之间的比较问题,正常情况下只能进行比较:== 或 !=,不能使用 > 或 <
Java 实现对象排序的方式:
- 自然排序:java.lang.Comparable
- 定制排序:java.lang.Comparator
java.lang.Comparable 接口
像 String、包装类等实现了 Comparable 接口,重写了 compareTo(Object obj) 方法,给出了比较两个对象大小的方式
重写 compareTo(Object obj) 的规则:
- 如果当前对象 this 大于形参对象 obj,则返回正整数
- 如果当前对象 this 小于形参对象 obj,则返回负整数
- 如果当前对象 this 等于形参对象 obj,则返回零
对于自定义类,如果需要排序,可以让自定义类实现 comparable 接口,重写 compareTo(Object obj) 方法,在 compareTo(Object obj) 方法中指明如何排序
java.lang.Comparator
当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码,或者实现了 java.lang.Comparable 接口的排序规则不适合当前的操作,可以考虑使用 Comparator 的对象来排序,强行对多个对象进行整体排序的比较
- 重写 compare(Object obj1, Object obj2) 方法,比较 obj1 和 obj2 的大小,如果方法返回正整数,则表示 obj1 大于 obj2、如果返回 0,表示相等、返回负整数,表示 obj1 小于 obj2
- 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制
- 还可以使用 Comparator 来控制某些数据结构(如有序 set 或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序
Comparable 接口与 Comparator 的使用对比:
- Comparable 接口的方式一旦一定,保证 Comparable 接口实现类的对象在任何位置都可以比较大小
- Comparator 接口属于临时性的比较
System 类
- System 类代表系统,系统级的很多属性和控制方法都放置在该类的内部,该类位于 java.lang 包
- 由于该类的构造器是 private 的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是 static 的
- 成员变量:
- System 类内部包含 in、out 和 err 三个成员变量,分别代表标准输入流(键盘输入)、标准输出流(显示器)和标准错误输出流(显示器)
- 成员方法:
- native long currentTimeMillis():返回当前的计算机时间,时间的表达格式为当前计算机时间和 GMT 时间(格林威治时间)1970年1月1日0时0分0秒所差的毫秒数
- void exit(int status):退出程序。其中 status 的值为 0 代表正常退出,非零代表异常退出。(使用该方法可以在图形界面编程中实现程序的退出功能)
- void gc():请求系统进行垃圾回收。至于系统是否立即回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况
- String getProperty(String key):获得系统中属性名为 key 的属性对应的值
- java.version:java 运行时环境版本
- java.home:java 安装目录
- os.name:操作系统的名称
- os.version:操作系统的版本
- user.name:用户的账户名称
- user.home:用户的主目录
- user.dir:用户的当前工作目录
Math 类
- abs:求绝对值
- acos、asin、atan、cos、sin、tan:三角函数
- sqrt:求平方根
- pow(double a, double b):求 a 的 b 次幂
- log:自然对数
- exp:e 为底指数
- max(double a, double b):求最大值
- min(double a, double b):求最小值
- random():返回 0.0 到 1.0 之间的随机数
- long round(double a):double 型数据 a 转换为 long 型(四舍五入)
- toDegrees(double angrad):弧度 -> 角度
- toRadians(double angdeg):角度 -> 弧度
BigInteger 与 BigDecimal
BigInteger
Integer 类作为 int 的包装类,能存储的最大整型值为 2^31 -1
,Long 类也是有限的。如果要表示再大的整数,不管是基本数据类型还是它们的包装类都无能为力
java.math 包的 BigInteger 可以表示不可变的任意精度的整数。BigInteger 除了与 Java 中基本整数操作相同外还提供:模算数、GCD 计算、质数测试、素数生成、位操作以及一些其他操作
构造器:BigInteger(String val):根据字符串构建 BigInteger 对象
常用方法:
- public BigInteger abs():返回 BigInteger 的绝对值的 BigInteger
- BigInteger add(BigInteger val):返回其值为(this + val)的 BigInteger
- BigInteger subtract(BigInteger val):返回其值为(this - val)的 BigInteger
- BigInteger multiply(BigInteger val):返回其值为(this * val)的 BigInteger
- BigInteger divide(BigInteger val):返回其值为(this / val)的 BigInteger。整数相除只保留整数部分
- BigInteger remainder(BigInteger val):返回其值为(this % val)的 BigInteger
- BigInteger[] divideAndRemainder(BigInteger val):返回包含(this / val)后跟(this % val)的两个 BigInteger 的数组
- BigInteger pow(int exponent):返回其值为(this^exponent)的 BigInteger
BigDecimal
一般的 Float 类和 Double 类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到 java.math.BigDecimal 类
BigDecimal 类支持不可变的、任意精度的有符号十进制定点数
构造器:
- public BigDecimal(double val)
- public BigDecimal(String val)
常用方法:
- public BigDecimal add(BigDecimal augend)
- public BigDecimal subtract(BigDecimal subtrahend)
- public BigDecimal multiply(BigDecimal multiplicand)
- public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
枚举类
枚举类的理解:类的对象是确定的且只有有限个,称此类为枚举类
- 需要定义一组常量时强烈建议使用枚举类
- 如果枚举类中只有一个对象,则可以作为单例模式的实现方式
定义枚举类
- JDK5.0 之前,自定义枚举类
- 自定义枚举类
- 声明对象的属性:private final 修饰
- 私有化(private)类的构造器,并初始化对象的属性
- 提供当前枚举类的多个对象:public static final 修饰
- 其他功能:
- 获取枚举类对象的属性
- 提供 toString()
- JDK5.0 新增,可以使用 enum 关键字定义枚举类
- 使用 enum 关键字定义枚举类:默认继承于 java.lang.Enum 类
- 提供当前枚举类的对象,多个对象之间用逗号 “,” 隔开,末尾对象用分号 “;” 结束:enumObjName(“stringContent”)
- 声明对象的属性:private final 修饰
- 私有化(private)类的构造器,并初始化对象的属性
- 提供当前枚举类的多个对象:public static final 修饰
- 其他功能:
- 获取枚举类对象的属性
- 提供 toString()
Enum 中的常用方法
Enum 类的主要方法:
- values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值
- valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的”名字”。若不是则出现运行时异常:IllegalArgumentException
- toString():返回当前枚举类对象常量的名称
使用 enum 关键字定义的枚举类实现接口的情况
- 情况一:实现接口,在 enum 类中实现抽象方法
- 情况二:让枚举类的对象分别实现接口中的抽象方法
- enumObjName(“stringContent”){ // 实现抽象方法}
注解(Annotation)
从 JDK5.0 开始,Java 增加了对元数据(MetaData)的支持。也就是 Annotation(注解)
- Annotation 其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理
- Annotation 可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被保存在 Annotation 的 “name=value” 对中
常见注解
使用 Annotation 时要在其前面增加 @ 符号,并把该 Annotation 当成一个修饰符使用,用于修饰它支持的程序元素
- 生成文档相关的注解:
- @author:标明开发该类模块的作者,多个作者之间使用逗号 “,” 分隔
- @version:标明该类模块的版本
- @see:标明参考转向,也就是相关主题
- @since:标明从哪个版本开始增加
- @param:对方法中某参数的说明,如果没有参数则不能写
- @return:对方法返回值的说明,如果方法的返回值类型是 void 则不能写
- exception:对方法可能抛出的异常进行说明,如果方法没有用 throws 显式抛出的异常则不能写
- 其中:
- @param、@return、@exception 这三个标记都是只用于方法的
- @param 的格式要求:@param 形参名 形参类型 形参说明
- @return 的格式要求:@return 返回值类型 返回值说明
- @exception 的格式要求:@exception 异常类型 异常说明
- @param 和 @exception 可以并列多个
- 在编译时进行格式检查(JDK 内置的三个基本注解)
- @Override:限定重写父类方法,该注解只能用于方法
- @Deprecated:用于表示所修饰的元素(类、方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
- @SuppressWarnings:抑制编译器警告
- @SuppressWarnings(“unused”)
- @SuppressWarnings({ “unused”, “rawtypes” })
- 跟踪代码依赖性,实现替代配置文件功能
- Servlet3.0 提供了注解(Annotation),使得不再需要在 web.xml 文件中进行 Servlet 的部署
- spring 框架中关于 “事务” 的管理
自定义注解
说明:
- 定义新的 Annotation 类型使用 @interface 关键字
- 自定义注解自动继承了 java.lang.annotation.Annotation 接口
- Annotation 的成员变量在 Annotation 定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。称为配置参数。类型只能是八种基本数据类型、String 类型、Class 类型、enum 类型、Annotation 类型、以上所有类型的数组
- 可以在定义 Annotation 的成员变量时为其指定初始值,指定成员变量的初始值可以使用 default 关键字
- 如果只有一个参数成员,建议使用参数名为 value
- 如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式为 “参数名=参数值”,如果只有一个参数成员,且名称为 value,可以省略 “value=”
- 没有成员定义的 Annotation 称为标记、包含成员变量的 Annotation 称为元数据 Annotation
注意:自定义注解必须配上注解的信息处理流程(使用反射)才有意义
自定义注解通常都会指明两个元注解:@Retention、@Target
步骤:
- 注解声明为:@interface
- 内部定义成员,通常使用 value
- 可以指定成员的默认值,使用 default 定义
- 如果自定义注解没有成员,标明是一个标识
JDK 中的元注解
JDK 的元 Annotation 用于修饰其他 Annotation 定义
JDK5.0 提供了 4 个标准的 meta-annotation 类型:
- @Retrntion:只能用于修饰一个 Annotation 定义,用于指定该 Annotation 的生命周期,@Retention 包含一个 RetentionPolicy 类型的成员变量,使用 @Retention 时必须为该 value 成员变量指定值:
- RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注解
- RetentionPolicy.CLASS:在 class 文件中有效(即 class 保留),当运行 Java 程序时,JVM 不会保留注解。(该值为默认值)
- RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时,JVM 会保留注解。程序可以通过反射获取该注解
- 只有声明为 RUNTIME 声明周期的注解,才能通过反射获取
- @Target:用于修饰 Annotation 定义,用于指定被修饰的 Annotation 能用于修饰哪些程序元素。@Target 也包含一个名为 value 的成员变量
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域
- LOCAL_VARIABLE:用于描述局部变量
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述参数
- TYPE:用于描述类、接口(包括注解类型)或 enum 声明
- @Documented:用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档。默认情况下,javadoc 是不包括注解的
- 定义为 Documented 的注解必须设置 Retention 值为 RUNTIME
- @Inherited:被它修饰的 Annotation 将具有继承性。如果某个类使用了被 @Inherited 修饰的 Annotation,则其子类将自动具有该注解
- 比如:如果把标有 @Inherited 注解的自定义注解标注在类级别上,子类则可以继承父类类级别的注解
- 实际使用较少
JDK8 中注解的新特性
可重复注解
JDK8 之前的写法:
- 实现 MyAnnotation:public @interface MyAnnotation{String value() default “str”;}
- 实现 MyAnnotations:public @interface MyAnnotations{MyAnnotations[] value();}
- 使用:@MyAnnotations({@MyAnnotation(value=”str1”), @MyAnnotation(value=”str2”)})
JDK8 新增支持的写法:
- 在 MyAnnotation 上声明 @Repeatable,成员值为 MyAnnotations.class
- 在 public @interface MyAnnotation{String value() default “str”;} 之上加上注解 @Repeatable(MyAnnotations.class)
- MyAnnotation 的 Target 和 Retention 等元注解与 MyAnnotations 保持一致
- @Retention(RetentionPolicy.RUNTIME)
- @Target({TYPE, FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
- @Inherited、…
类型注解
JDK8 之后,关于元注解 @Target 的参数类型 ElementType 枚举值多了两个:TYPE_PARAMETER、TYPE_USE
在 Java8 之前,注解只能是在声明的地方所使用,Java8 开始,注解可以应用在任何地方:
- ElementType.TYPE_PARAMETER:表示该注解能写在类型变量的声明语句中(如:泛型声明)
- ElementType.TYPE_USE:表示该注解能写在使用类型的任何语句中