gtb-6 Java-advanced

162 阅读14分钟

Java 进阶

异常处理

java使用 try{...} catch语句来捕获异常,异常(Throwable class) 分为

  • Error:无需catch的严重错误
  • Exception:被catch的类型,分为RuntimeException(具体情况具体分析,没有强制捕获),和非RuntimeException(必须捕获并进行处理,否则编译器会报错)

(这里和js不同,js一般catch error就可以了)

异常语句

  1. 抛出异常使用throw+new+ Exception
  2. catch Exception中如有继承关系的Exception ,子类必须先放在前面
  3. 可以使用|匹配类似操作的异常
  4. 可选择finally表示最后必须执行的操作
  5. e.printStackTrace() 方法——将异常的堆栈跟踪打印到控制台,追踪异常产生的地址
  6. 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) :
  1. 变量定义时初始化,String初始化为""空字符串

  2. 使用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->不实现类,而创建接口对象

    1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;

    2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:

    • 使用的ClassLoader,通常就是接口类的ClassLoader

    • 需要实现的接口数组,至少需要传入一个接口进去;

    • 用来处理接口方法调用的InvocationHandler实例。

    1. 将返回的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接口里面的修饰符@

分类:

  1. 编译器使用的,不会进入.class文件

    @Override (让编译器检查是否正确实现覆写)

  2. 工具处理的注解,底层编译进入.class文件

  3. 程序运行时能够读取的注解,其中包含可省略的参数value

定义注解

  1. 使用@interface定义多个参数和默认值
  2. 必须使用@Target指定注解可以应用的范围
  3. 设置@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>()

注意:

  1. 泛型不能用于静态方法,用其他静态方法区分
  2. 可以使用多个泛型,eg Map<K,V>
  3. 泛型不能是基本类型,不能反射获取带泛型的class
  4. extends super泛型可以读(向上转型)不能写(不确保安全-向下转型不一定成功)

函数式编程

类似react编程方式理解纯函数:

  1. 任意一个函数,只要输入是确定的,输出就是确定的(没有副作用)
  2. 高阶函数:将函数本身作为参数传给另一个参数;可以返回一个函数

函数式编程 可以称之为 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特点:

  1. stream中的元素是实时计算的(与list不同,list先创建已有对象后计算)
  2. stream可以轻易的转化为另一个Stream而不改变stream本身
  3. stream惰性计算:stream相互转换时实际上只存储了转换规则,并没有任何计算发生。-> 链式操作

用法:创建一个Stream,然后做若干次转换,最后调用一个求值方法获取真正计算的结果

创建Stream

  1. 基于数组或Collection -> .stream() 元素固定

    Stream<String> s1 = Arrays.stream(new String[]{"A","B"})
    Stream<String> s2 = List.of("C","D").stream()    
    
  2. 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)
    
  3. Suppier-> Stream.iterate() 无限序列,类似reduce

    //eg.每个序列都进行操作
    Stream<Integer> infinite = Stream.iterate(0,i->i+1);
    
  4. 第三方接口

    Files.lines() Pattern.splitAsStream() Stream.of()

  • 基本类型的Stream

    Stream无法使用,而会有效率问题,java提供:

    IntStream LongStream DoubleStream 基本类型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())
  • 输出为数组 .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的情况

方法

  • 创建

    1. Optional.empty()
    2. 非空 Optional.of("a","b)
    3. Option.ofNullable()判断变量为空返回empty,不为空返回Optional.of()
  • 中间操作(返回值仍为Option)

    1. filter() :optional为empty不会调用其方法
    2. map() :结果用Optional对象包裹起来
    3. flatMap() :多层optional包裹的拍平
    4. stream() :optional对象转化为stream对象,可以调用stream.map()等api操作
  • 返回具体值

    1. isPresent -> 是否存在true /false

      1. ifPresent -> 函数
    2. get -> 存在返回value,没有throw exception

    3. orElse -> 为null时进行的操作

      1. 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)

常用类:

  • 时间戳 instant

    Instant.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")) //指定时区
      
    • ZoneDateTimeLocalDateTime可以相互转换 -> 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 Period

    • Duration : 两个时刻之间的时间间隔 -> LocalDateTime LocalTime
    • Period:两个日期之间的天数 -> 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实际上是多线程模型。

多线程经常需要读写共享数据,并且需要同步。

创建新线程

方法:

  1. 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....
        }
    }
    
  2. 创建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毫秒

线程的状态

  1. Java线程对象Thread的状态包括:
  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。
  1. 通过对另一个线程对象调用join()方法可以等待其执行结束;t.join()

可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;

对已经运行结束的线程调用join()方法会立刻返回。

线程操作

中断线程

含义:其他线程给该线程发信号,该线程收到后执行run()方法,使得自身线程立刻结束运行

  • 方法一:子线程调用interrupt() 方法
  1. 对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException
  2. 目标线程检测到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
  1. 通过标志位判断需要正确使用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();
    
  • 不需要同步的情况
  1. 对JVM定义的单个原子操作。原子操作指的是不能被中断的一个或一系列操作。如基本类型和引用类型赋值。int i=m List<String> list = anotherlist
  2. 不可变对象

死锁

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

死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;

避免死锁的方法是多线程获取锁的顺序要一致,队列先进先出顺序。