🚩Java 基础

  • 几乎所有对象实例都存在堆中,基本数据类型只要没有被 static 修饰,也存放在堆中。
  • 数值类缓存了[-128,127],Character 缓存了 [0,127]范围内的数据。
1
2
3
Integer a = 40; // 等价于 Integer a = Integer.valueOf(40);
Integer b = new Integer(40); 
log.info(a==b); // false 因为 a 是常量池对象b 是堆内存对象

元注解

  • @Retention用于标明注解被保留的阶段 RetentionPolicy

    • SOURCE 源文件保留
    • CLASS 编译期保留,默认值(.class:RuntimeInvisibleAnnotations)
    • RUNTIME 运行期保留,可通过反射去获取注解信息(.class: RuntimeVisibleAnnotations)
  • @Target用于标明注解使用的范围 ElementType

  • @Inherited用于标明注解可继承

反射

  1. 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;

  2. 每个类都会有一个与之对应的Class实例,从而每个类都可以获取method反射方法,并作用到其他实例身上;

  3. 反射也是考虑了线程安全的,放心使用;

  4. 反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;

  5. 反射调用多次生成新代理Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;

    1. invoke时,是通过 MethodAccessor 进行调用的,而 MethodAccessor 是个接口,在第一次时调用 acquireMethodAccessor() 进行新创建。
    2. 进行 ma.invoke(obj, args); 调用时,调用 DelegatingMethodAccessorImpl.invoke();最后被委托到 NativeMethodAccessorImpl.invoke()
    3. 在ClassDefiner.defineClass方法实现中,每被调用一次都会生成一个DelegatingClassLoader类加载器对象 ,这里每次都生成新的类加载器,是为了性能考虑,在某些情况下可以卸载这些生成的类,因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的,如果用了原来的类加载器,那可能导致这些新创建的类一直无法被卸载。而反射生成的类,有时候可能用了就可以卸载了,所以使用其独立的类加载器,从而使得更容易控制反射类的生命周期。
  6. 当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;

  7. 调度反射方法,最终是由jvm执行invoke0()执行;

API vs SPI?

SPI - “接口”位于“调用方”所在的“包”中

  • 概念上更依赖调用方。
  • 组织上位于调用方所在的包中。
  • 实现位于独立的包中。
  • 常见的例子是:插件模式的插件。

使用SPI机制的缺陷:

  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

API - “接口”位于“实现方”所在的“包”中

  • 概念上更接近实现方。
  • 组织上位于实现方所在的包中。
  • 实现和接口在一个包中。

happens-before规则?

Java 虚拟机在进行代码编译优化时,会出现指令重排序问题,为了避免编译优化对并发编程安全性的影响,Java 虚拟机需要 happens-before 规则限制或禁止编译优化的场景,保证系统并发的安全性。

为什么金额不能使用double,要使用BigDecimal?

浮点数不精确的根本原因在于尾数部分的位数是固定的,一旦需要表示的数字的精度高于浮点数的精度,那么必然产生误差!这在处理金融数据的情况下是绝对不允许存在的。

强引用、软引用、弱引用、幻象引用有什么区别?

  • 强引用,就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。

  • 软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

    • 反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;
  • 弱引用(WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。

  • 虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,我在专栏上一讲中介绍的 Java 平台自身 Cleaner 机制等,也有人利用幻象引用监控对象的创建和销毁

JIT

引入JIT(Just-In-Time)编译的主要目的是提高程序的执行性能。传统的编译方式是提前将源代码编译成机器码,然后在运行时执行。而JIT编译是在程序运行时将部分代码进行动态编译成机器码,以提高执行效率。

JIT编译器会在运行时对热点代码进行监测和分析,热点代码指的是频繁执行的代码块或方法。一旦确定某段代码是热点代码,JIT编译器会将其编译成机器码,并缓存起来,以便下次执行时直接使用。这样可以避免每次执行都需要进行解释和执行源代码的性能损耗。

逃逸分析

是一种静态分析技术,用于确定程序中的对象是否逃逸出方法的作用域。逃逸指的是对象在方法外被引用或传递给其他方法使用的情况

  • 栈上分配和标量替换。栈上分配将对象分配在线程栈上,减少堆内存的使用。
  • 标量替换将对象拆分为独立的字段,存储在寄存器或栈上。

ASCII码,Unicode和utf-8的区别是什么

字符ASCII码Unicodeutf-8
A0100000100000000 0100000101000001
中文无法表示01001110 0010110101001110 00101101
  • ASCII码:用 8 bit 来表示世界通用的英文、数字、标点。
  • Unicode:用 2B 来表示每个字符。
  • utf-8:英文字母 1B,汉字 3B,可以兼容 ASCII 码。
  • GB2312/GBK:英文字母 1B,汉字 2B

Lambda 表达式 - 匿名函数

  1. 减少代码冗余,增强易读性
  2. 实现闭包:内层函数可以引用包含在它外层的函数的变量

clone() 方法的缺陷

Java中的clone()方法是用于创建对象的副本的方法,它是从Object类继承而来的。有如下缺陷:

  1. 浅拷贝问题

  2. 破坏封装性:clone()方法是protected访问修饰符,它只能在对象的类及其子类中访问。为了使用clone()方法,需要在需要克隆的类中实现Cloneable接口,并实现clone()方法。这会导致在类的设计中暴露出一些细节,破坏了封装性。

  3. 缺乏类型安全:clone()方法返回的是Object类型,需要进行类型转换才能得到具体的对象。这增加了出错的可能性,并且在编译时无法进行类型检查。

  4. 构造函数不会被调用:在使用clone()方法创建对象副本时,对象的构造函数不会被调用。这意味着无法执行构造函数中的初始化操作,可能导致克隆对象的状态不完整或不一致。

  5. 更好的替代方案:Java提供了其他更好的替代方案来创建对象的副本,如使用拷贝构造函数、工厂方法、序列化等。这些方式在实现上更直观、更灵活,并且可以更好地满足特定的需求。

MultipartFile和InputStream是什么关系?

1
2
3
4
5
6
7
8
9
public interface MultipartFile extends InputStreamSource {...}
public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}
public abstract class InputStream implements Closeable {...}
public interface Closeable extends AutoCloseable{}
public interface AutoCloseable {
    void close() throws Exception;
}
  1. 我们在使用MultipartFile作为参数传递的时候,可以将MultipartFile声明为一个数组,这样就能支持多文件传输,如果只需要传输一个文件,则去掉数组就好了。
  2. 可以根据MultipartFile的getSize方法来获取到传输文件的大小,这样就能限定传输过来的文件的大小了。

泛型会在编译期擦除,为什么还要使用?

泛型,本质上是编译器的行为,为了保证引入泛型机制,但不创建新的类型。使用泛型,可以在编译期间进行类型检测。如果使用 Object 代替,可读性差,并且需要手动添加强制类型转换。

泛型的限制

  • 泛型参数不能是基本类型,而应当用包装类,因为基本类型不是 Object 的子类。
  • 不能实例化泛型数组
  • 泛型无法使用 instance of 和 getClass() 进行类型判断
  • 泛型接口不能重载,否则擦除后会冲突。
  • 不能使用 static 修饰泛型变量。

父子关系

  • ChildNode<Circle>Node<Circle> 的子类
  • Node<Circle> 不是 Node<Shape> 的子类

T extends xxx 和 ? extends xxx 有什么区别

前者用于定义泛型类和方法,擦除后为 xxx 类型;后者用于声明方法形参,接收 xxx 和其子类型。

  • ? extends xxx:只能调用 get() ,只能读
  • ? super xxx: 只能调用 set(),只能写

String 常量表达式理解

1
2
3
4
5
6
7
8
9
final String h1 = "hello"; // 编译期常量
String h2 = "hello";
final String h3 = h2; // 运行时常量
String s1 = h1 +"World";// 反编译为 "helloWorld"
String s2 = h2 +"World";
String s3 = h3 +"World";
log.info(s1=="helloWorld"); // true
log.info(s2=="helloWorld"); // false
log.info(s3=="helloWorld"); // false
0%