Java 进阶
异常处理
java使用 try{...} catch语句来捕获异常,异常(Throwable class) 分为
- Error:无需catch的严重错误
- Exception:被catch的类型,分为RuntimeException(具体情况具体分析,没有强制捕获),和非RuntimeException(必须捕获并进行处理,否则编译器会报错)
(这里和js不同,js一般catch error就可以了)
异常语句
- 抛出异常使用
throw+new+ Exception - catch Exception中如有继承关系的Exception ,子类必须先放在前面
- 可以使用
|匹配类似操作的异常 - 可选择
finally表示最后必须执行的操作 - e.printStackTrace() 方法——将异常的堆栈跟踪打印到控制台,追踪异常产生的地址
- Throwable.getCause() 方法 —— 捕获原始异常,如果为null,则为根异常
void p(){
throw new IOException();
}
try{
p();
}catch (FileNotFoundException e){
//..file not found
}catch(SecurityException e){
//...no read permission
}catch(IOException | NumberFormatException e){
//...
}
catch(Exception e){
//...other error
}finally{
//every catch through here
}
try with resources
当在try语句内new一个实例并需要捕获异常时,可以通过try with resources语法糖简化写法,也可以使用final修饰符
即使在内部抛出异常,资源也会被正确关闭。这是因为 try-with-resources 会自动调用资源的 close() 方法,而不管内部代码是否抛出异常。
注意:定义多个resource时,最先定义的resourcesexception最后关闭(栈-先进后出
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
//使用传统写法
Scanner scanner = null;
try {
scanner = new Scanner(new File("test.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
自定义异常
业务逻辑中处理RuntimeException中可以自定义class继承它,并以此提供多种构造方法处理
public class BaseException extends RuntimeException{
public BaseException(){
super()
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
//子类
public class UserNotFoundException extends BaseException{
}
NullPointerException
一个对象为null,调用其方法和字段会产生nullpointerexception
- 解决(尽量少使用null) :
-
变量定义时初始化,String初始化为""空字符串
-
使用null判断的地方可以使用Optional
public Optional<String> readFormFile(String file){ if(!fileExit(file)){ return Optional.empty(); } }
-
定位nullpointexception
在文件中(Main.java)开启:
java -XX:+ShowCodeDetailsInExceptionMessages Main.java可以在
NullPointerException的详细信息中看到类似... because "<local1>.whichpropertity" is null
断言 && Logging
- 断言 assert
使用断言判断,断言失败抛出assertionError
assert x >=0; "x must >=0"
//如果X<0 ,抛出异常并带上消息x must >=0
正式开发中很少使用,一般编写单元测试
- Logging
类似于System.out.print 但是可以设置输出形式,有点像js中的debugger,内置类java.util.logging.Logger
反射
在运行期间,对某个实例一无所知的情况下,通过实例获取class
getClass()|obj.Class
原理:JVM为每个加载class和interface都创建了对应的Class实例来保存class及interface的所有信息,在加载的时候动态加载
通过反射可以:
- 访问字段- Field实例
通过Class实例的方法可以获取:getField(),getFields(),不包括父类-》getDeclaredField(),getDeclaredFields();
通过Field实例可以获取字段信息:getName(),getType(),getModifiers();
通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。
Object obj=new Person("lili")
Class c=obj.getClass();
Field f = c.getDeclaredFiled("name");
Object v=f.get(obj);
- 调用方法-Method实例
- 调用构造方法-Constructor实例
- 获取继承关系-getSuperClass() / getInterfaces()
动态代理
动态代理:在运行期间动态创建某个interface实例
-
一般而言:编写interface->class implements interface ->instance
-
动态代理:编写interface->不实现类,而创建接口对象
-
定义一个
InvocationHandler实例,它负责实现接口的方法调用; -
通过
Proxy.newProxyInstance()创建interface实例,它需要3个参数:
-
使用的
ClassLoader,通常就是接口类的ClassLoader; -
需要实现的接口数组,至少需要传入一个接口进去;
-
用来处理接口方法调用的
InvocationHandler实例。
- 将返回的
Object强制转型为接口。
-
interface Say{...} //接口
//具体调用方法
InvocationHandle h =new InvocationHandle();{
@override
public void hiWorld(){...}
}
//动态代理
Say s=(Say) Proxy.newProxyInstance(
Say.class,getClassLoader(),
new Class[] {Say.class},
h
)
//成功运用
s.hiWorld() //sucessfully
注解
注解(Annotation) : 放在java代码前面的一种特殊注释,有点类似Nest.js接口里面的修饰符@
分类:
-
编译器使用的,不会进入.class文件
@Override (让编译器检查是否正确实现覆写)
-
工具处理的注解,底层编译进入.class文件
-
程序运行时能够读取的注解,其中包含可省略的参数value
定义注解
- 使用@interface定义多个参数和默认值
- 必须使用@Target指定注解可以应用的范围
- 设置@Retention(RetentionPolicy.RUNTIME) 表示运行期间读取
@Target(ElementType.METHOD) //@Report可以使用在方法上
@Target({
ElementType.METHOD,
ElementType.FIELD
})//@Report可以使用在字段或者方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface Report{
String value() defalut "";
}
泛型
类似于Typescript中的泛型,编写模板代码来适应任意类型,在接口中定义,implement该接口的class必须实现该泛型
eg. ArrayList<Number>() ArrayList<String>()
注意:
- 泛型不能用于静态方法,用其他静态方法区分
- 可以使用多个泛型,eg
Map<K,V> - 泛型不能是基本类型,不能反射获取带泛型的class
extendssuper泛型可以读(向上转型)不能写(不确保安全-向下转型不一定成功)
函数式编程
类似react编程方式理解纯函数:
- 任意一个函数,只要输入是确定的,输出就是确定的(没有副作用)
- 高阶函数:将函数本身作为参数传给另一个参数;可以返回一个函数
函数式编程 可以称之为 Lambda表达式(和js区别就是const fn=>{} ,js用的=> ,java用的->
要求:
-
FunctionalInterface : 只定义了单方法的接口(除去defalut \static \固定接口定义的其他方法只有一个方法)
要求:签名一致(传入的参数类型、数量、返回的类型)
-
方法引用
类名::实例方法要求:签名一致(传入的参数类型、返回类型一致)
使用:
静态方法
Main::compareto实例方法
String::sayhi实例的类型被看作第一个参数类型、构造方法
Person::new实例类型被看作返回类型(new一个person类型返回//普通 Consumer<TodoTask> ts = (ts->System.out.println(ts)) //方法引用 Consumer<TodoTask> ts = (System.out::println);
java内置了很多函数式接口,常用Function Predicate Supplier Consumer
Stream API
感觉有点像js里的数组方法
stream特点:
- stream中的元素是实时计算的(与list不同,list先创建已有对象后计算)
- stream可以轻易的转化为另一个Stream而不改变stream本身
- stream惰性计算:stream相互转换时实际上只存储了转换规则,并没有任何计算发生。-> 链式操作
用法:创建一个Stream,然后做若干次转换,最后调用一个求值方法获取真正计算的结果
创建Stream
-
基于数组或
Collection->.stream()元素固定Stream<String> s1 = Arrays.stream(new String[]{"A","B"}) Stream<String> s2 = List.of("C","D").stream() -
Suppier->Stream.generate()无限序列Stream<String> s = Stream.generate(Suppier<String> sp); //eg.每个序列都是"same" Stream<String> same = Stream.generate(()->"same"); //对无限序列进行操作:需要先用limit变为有限序列 same.limit(10).forEach(System.out::printIn) -
Suppier->Stream.iterate()无限序列,类似reduce//eg.每个序列都进行操作 Stream<Integer> infinite = Stream.iterate(0,i->i+1); -
第三方接口
Files.lines()Pattern.splitAsStream()Stream.of()
-
基本类型的Stream
Stream无法使用,而会有效率问题,java提供:
IntStreamLongStreamDoubleStream基本类型Stream
相关方法
实际上就是js es6的新增方法,只不过对象换成了stream
转换
tips : 不触发计算,返回新的stream
- map() : 复制原stream对象并进行操作
- filter() : 只筛选出满足条件的新stream
- sorted(): 要求元素实现Comparable接口,可以往()里面传入函数
- distinct():去重!!就没必要转换为Set了
聚合
tips: 最终触发计算
- reduce() : 累加计算结果(pre,cur) -> pre+cur
- collect(Collectors....) :输出为其它形式
- count() : 返回元素个数
- max() \ min() \ sum() \ average()
合并
- concat(s1,s2): 合并两个stream为一个stream
- flatMap() : 元素映射为stream(js理解为拍平数组)
截取
- skip(n) : 跳过当前stream的前n个元素
- limit(n) : 截取当前stream的最多前n个元素
其他
- parallel() : 转化为一个可以并行处理的stream
- allMatch() :所有都满足条件
- anyMatch() :至少有一个满足条件
- forEach() :循环遍历每个元素
- findFirst() : 用于从一个 Stream 序列中查找第一个元素。这个方法返回一个
Optional类型的实例,它可能包含序列的第一个元素,也可能为空(如果序列为空或为null)。
输出为其他类型
-
输出为List
.collect(Collectors.toList())- 输出为Set
.collect(Collectors.toSet())
- 输出为Set
-
输出为数组
.toArray(构造方法::new)eg..toArray(String[]::new) -
输出为Map
Map<String,String> map=stream.collect(Collectors.toMap( s -> s.substring(0,s.indexOf(':')), //key s -> s.substring(s.indexOf(':')+1); //value )) -
分组输出
Collectors.groupingBy( classifyingFunction,//确定分类依据 downstreamCollector,//如何收集分类结果(结果容器 mergeFunction,//解决多个元素映射一个键的冲突 )
Optional API
含义:一个对象可以包含null或者non-null的值,并提供一些方法供我们判断
使用:一般与stream结合起来链式调用统一处理为null的情况,使用理念有些类似promise
举例:
String name = task.getSubTask()
.flatMap(TodoTask::getSubTask)
.map(TodoTask::getName)
.orElse("Not found"); //orElse统一处理为null的情况
方法
-
创建
- 空
Optional.empty() - 非空
Optional.of("a","b) Option.ofNullable()判断变量为空返回empty,不为空返回Optional.of()
- 空
-
中间操作(返回值仍为Option)
- filter() :optional为empty不会调用其方法
- map() :结果用Optional对象包裹起来
- flatMap() :多层optional包裹的拍平
- stream() :optional对象转化为stream对象,可以调用stream.map()等api操作
-
返回具体值
-
isPresent -> 是否存在true /false
- ifPresent -> 函数
-
get -> 存在返回value,没有throw exception
-
orElse -> 为null时进行的操作
- orElseThrow -> 为null时抛出错误
-
Date Time API
时间相关概念:
Timestamp :时间戳 eg.17218810041970年开始一秒加到现在
Calendar:日历-日期-时间
LocalTime :本地时间
Time Zone:时区(根据经度不一样)eg.UTC+08:00 东八区 12:33:12 UTC 偏移量+缩写
Locale : 计算机在不同语言环境表示的日期 eg.2021 年 8 月 11 日 星期三 Wednesday August 11, 2021
ISO 8601:日期表示的格式
- 日期:yyyy-MM-dd
- 时间:HH:mm:ss
- 带毫秒的时间:HH:mm:ss.SSS
- 日期和时间:yyyy-MM-dd'T'HH:mm:ss
- 带毫秒的日期和时间:yyyy-MM-dd'T'HH:mm:ss.SSS
API(JAVA8)
常用类:
-
时间戳
instantInstant.now()获取当前时间戳ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());和ZoneDateTime相互转化 -
本地日期和时间
LocalDate日期 LocalTime时间 LocalDateTime日期时间//1.获取当前日期or时间 LocalDate.now()//当前日期 //2.创建/传入指定时间 LocalTime.of(12,33,12) //12:33:12 LocalDate day=LocalDate.parse("2024-7-25") //3.调整时间 LocalDate d2=day.withMonth(5);//2024-5-25 //4.比较前后 d2.isBefore(day) day.isAfter(d2)DateTimeFormatter格式化日期
LocalDateTime time=LocalDateTime.of(2024,07,25,22,56,21); DateTimeFormatter dtf=DDateTimeFormatter,ofPattern("yyyy-MM-dd HH:mm"); LocalDateTime stime = LocalDateTime.parse(time,dtf); System.out.printIn(stime); //2024-07-25 22:56 -
带时区的日期和时间
ZonedDateTime-> ZoneId.of()-
创建ZonedDateTime
ZonedDateTime zbj = ZonedDateTime.now() //默认时区 ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("America/New_York")) //指定时区 -
ZoneDateTime和LocalDateTime可以相互转换 -> ZoneId.of()//ldt -> zdt : atZone添加ZoneId LocalDateTime ldt = LocalDateTime.of(2024, 9, 15, 15, 16, 17); ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault()); ZonedDateTime zny = ldt.atZone(ZoneId.of("America/New_York")); //zdt -> ldt LocalDateTime ldt = zdt.toLocalDateTime(); -
时区转换
withZoneSameInstant()zonedDateTime zny=zbj.withZoneSameInstant(ZoneId.of("America/New_York"))
-
-
时间间隔
Duration PeriodDuration: 两个时刻之间的时间间隔 -> LocalDateTime LocalTimePeriod:两个日期之间的天数 -> LocalDate
//获取间隔between\ until Duration d=Duration.between(LocalDateTime1,LocalDateTime2); Period p=LocalDate.of(2024,6,22).until(LocalDate.of(2024,7,1)) //直接创建 ofXXX \ parse"P日期...T时间“形式 Duration d3 = Duration.ofHours(10); Duration d4 = Duration.parse("P3DT2H3M")//3day 2hour 3minuts
其他常用方法
- ChronoUnit.DAYS.between() :(long) 计算两个LocalDate之间天数
- date.getDayOfWeek() :获取日期是周几
多线程
操作系统调度的最小单位-线程
一个java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在其内部又可以启动多个线程,还有JVM垃圾回收的线程。所以,java实际上是多线程模型。
多线程经常需要读写共享数据,并且需要同步。
创建新线程
方法:
-
extends一个派生类,override run方法
public class Main{ public static void main(String[] args){ Thread t = new MyThread(); t.start() //启动 } } class myThread extends Thread{ //class实现 @Override public void run(){ // thread do some work.... } } -
创建Thread实例时传入一个Runable实例 -> start()
Thread t= new Thread(()->{System.out.printIn("start")}) //普通写法 public class Main { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 启动新线程 } } class MyRunnable implements Runnable { //interface实现 @Override public void run() { System.out.println("start new thread!"); } }
tips: 调用的都是start() 方法哦!
- 线程调用顺序(这里和js大大的不同!!!)
只可以确定主线程自己的执行顺序,其他线程自己的执行顺序。多个线程开始同时运行时,并且由操作系统调度,程序本身无法确定具体哪一个线程的调度顺序。
Thread.sleep(n):强迫线程暂停n毫秒
线程的状态
- Java线程对象
Thread的状态包括:
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行
run()方法的Java代码; - Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行
sleep()方法正在计时等待; - Terminated:线程已终止,因为
run()方法执行完毕。
- 通过对另一个线程对象调用
join()方法可以等待其执行结束;t.join()
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用join()方法会立刻返回。
线程操作
中断线程
含义:其他线程给该线程发信号,该线程收到后执行run()方法,使得自身线程立刻结束运行
- 方法一:子线程调用interrupt() 方法
- 对目标线程调用
interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException; - 目标线程检测到
isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
public class Main{
public static void main(String[] args){
Thread t = new MyThread();
t.start() //启动
Thread.sleep(10) //暂停10毫秒
t.interrupt() //中断t线程
t.join() //等待t线程结束
}
}
class myThread extends Thread{ //class实现
@Override
public void run(){
if(!isInterrupted){
//...dosomework
}
}
}
- 方法二:子线程调用running()方法,子线程使用
volatile定义running
- 通过标志位判断需要正确使用
volatile关键字;
volatile关键字解决了共享变量在线程间的可见性问题。
public class Main{
public static void main(String[] args)throw InterruptedException {
Thread t = new MyThread();
t.start() //启动
Thread.sleep(10) //暂停10毫秒
t.running = false;
}
}
class myThread extends Thread{ //class实现
public volatile boolean running=true;
@Override
public void run(){
if(!isInterrupted){
//...dosomework
}
}
}
守护线程
含义:有一种线程需要在在JVM结束后也不会关闭(定时器),为此,可以对其设置守护线程
Thread t = new Thread();
t.setDaemo(); //要在调用start()之前使用
t.start();
线程同步
多线程同时读写共享变量时,可能会造成逻辑错误,因此需要通过synchronized加锁同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
//simple use...
synchronized(object.instance) { //获取锁
//do some work
}//释放锁
//eg
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start(); //启动
dec.start();
add.join();
dec.join();//等待结束
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock = new Object(); //线程对同一个实例进行操作
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) { //获取锁
Counter.count += 1;
}//释放锁
}
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) { //获取锁
Counter.count -= 1;
}//释放锁
}
}
}
- 通常使用:封装
synchronized
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count -= n;
}
}
public int get() {
return count;
}
}
//线程直接调用即可
var c1 = Counter()
var c2 = Counter()
new Thread(()->{c1.add()}).start();
- 不需要同步的情况
- 对JVM定义的单个原子操作。原子操作指的是不能被中断的一个或一系列操作。如基本类型和引用类型赋值。
int i=mList<String> list = anotherlist - 不可变对象
死锁
Java的synchronized锁是可重入锁;eg. 在一个锁内部调用了其他锁
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n); //在add锁内部又使用了dec锁
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
避免死锁的方法是多线程获取锁的顺序要一致,队列先进先出顺序。