1.1. Java 基础

1.1.1. 谈谈你对JAVA平台的理解

第一,Java是一种面向对象的语言,显著的特性有两个。

  1. 跨平台能力:一次编译,到处执行。
  2. 垃圾回收,自动回收分配内存。

第二,通常将JAVA分为编译期和运行时

  • 在编译期,都是通过javac编译成.class的字节码文件。
  • 在运行时Java通过类加载器加载字节码文件,解释或者编译执行。

第三,Java可以分为解释执行和编译执行。在JDK8中,是采用解释和编译混合模式的。

  • 解释执行:通过JVM内嵌的解释器将字节码文件转换成机器码。

  • 编译执行:在Hotspot JVM中,提供了JIT(Just-In-Time)编译器,即动态编译器,能够将运行时热点代码编程机器码。

    对于编译执行,在server模式下使用C2编译器,它优化长时间运行的服务器端程序,通常会调用上万次以收集足够的信息进行高效编译;在client模式下使用C1编译器,它用于对启动速度敏感的应用,例如桌面应用。

1.1.2. Java语言特点

  1. 平台无关性,摆脱硬件束缚,"一次编写,到处运行"。
  2. 相对安全的内存管理和访问机制,避免大部分内存泄漏和指针越界。
  3. 热点代码检测和运行时编译及优化,使程序随运行时间增长获得更高性能。
  4. 完善的应用程序接口,支持第三方类库。

1.1.3. Java 如何实现平台无关?

  • JVM: Java 编译器可生成与计算机体系结构无关的字节码指令,字节码文件不仅可以轻易地在任何机器上解释执行,还可以动态地转换成本地机器代码,转换是由 JVM 实现的,JVM 是平台相关的,屏蔽了不同操作系统的差异。
  • 语言规范: 基本数据类型大小有明确规定,例如 int 永远为 32 位,而 C/C++ 中可能是 16 位、32 位,也可能是编译器开发商指定的其他大小。Java 中数值类型有固定字节数,二进制数据以固定格式存储和传输,字符串采用标准的 Unicode 格式存储。

1.1.4. JDK与JRE区别

  • JRE:是 Java 的运行环境。
  • JDK:Java 开发工具包。

1.1.5. Java 按值调用还是引用调用?

按值调用指方法接收调用者提供的值,按引用调用指方法接收调用者提供的变量地址。

Java 总是按值调用,方法得到的是所有参数值的副本,传递对象时实际上方法接收的是对象引用的副本。方法不能修改基本数据类型的参数,如果传递了一个 int 值 ,改变值不会影响实参,因为改变的是值的一个副本。

可以改变对象参数的状态,但不能让对象参数引用一个新的对象。如果传递了一个 int 数组,改变数组的内容会影响实参,而改变这个参数的引用并不会让实参引用新的数组对象。

这一次,彻底解决Java的值传递和引用传递

1.1.6. 浅拷贝和深拷贝的区别?

  • 浅拷贝: 只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象可能影响原对象,不安全。
  • 深拷贝: 完全拷贝基本数据类型和引用数据类型,安全。

1.1.7. 什么是反射?

在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。缺点是破坏了封装性以及泛型约束。反射是框架的核心,Spring 大量使用反射。

1.1.8. Class 类的作用?如何获取一个 Class 对象?

在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。

获取 Class 对象:① 类名.class 。②对象的 getClass方法。③ Class.forName(类的全限定名)

1.1.9. 什么是注解?什么是元注解?

注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override 标识一个方法是重写方法。

元注解是自定义注解的注解,例如:

  • @Target:约束作用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。
  • @Rentention:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。
  • @Documented:表明这个注解应该被 javadoc 记录。

1.1.10. 什么是泛型,有什么作用?

泛型本质是参数化类型,解决不确定对象具体类型的问题。泛型在定义处只具备执行 Object 方法的能力。

泛型的好处:① 类型安全,放置什么出来就是什么,不存在 ClassCastException。② 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。③ 代码重用,合并了同类型的处理代码。

1.1.11. 泛型擦除是什么?

泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。例如定义 List<Object>List<String>,在编译后都会变成 List

定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。如果没有限定类型就会替换为 Object,如果有限定类型就会替换为第一个限定类型,例如 <T extends A & B> 会使用 A 类型替换 T。

JDK8 新特性有哪些?

  • lambda 表达式:允许把函数作为参数传递到方法,简化匿名内部类代码。
  • 函数式接口:使用 @FunctionalInterface 标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。
  • 方法引用:可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
  • 接口:接口可以定义 default 修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
  • 注解:引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
  • 类型推测:加强了类型推测机制,使代码更加简洁。
  • Optional 类:处理空指针异常,提高代码可读性。
  • Stream 类:引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、filter 按条件过滤、limit 取前 n 个元素、skip 跳过前 n 个元素、map 映射加工、concat 合并 stream 流等。
  • 日期:增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。
  • JavaScript:提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。

1.2. 异常

1.2.1. 对比 Exception 和 Error

Exception 和 Error 都是继承了 Throwable 类。在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。

  • Error :指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
  • Exception :程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。

1.2.2. 运行时异常与一般异常有什么区别?

Exception 又分为编译期(checked)异常和运行时(unchecked)异常。

  • 编译期异常:这是编译期检查出的异常,必须处理。类似于IOException
  • 运行期异常:在运行时,检查异常。具体根据需要来判断是否需要捕获,并不会在编译期强制要求。运行时异常代表的是编程错误:
    • 无法预料的错误,比如从你控制范围之外的传递进来的null引用。
    • 应当在代码中进行检查的错误,导致的异常。例如ArrayIndexOutOfBoundsException

1.2.3. try-catch-finally、throw、throws区别

  • try-catch-finally:捕获异常,对异常进行处理。
  • throw:将异常传递给调用者,并结束当前执行。出现在函数体。
  • throws:声明抛出,由该方法的调用者处理。出现在函数头。

1.2.4. 异常处理的两个原则

  1. 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。例如,使用InterruptedException去捕获Thread.sleep() 抛出的异常。
  2. 不要生吞异常。 应当将异常抛出,输出到日志(Logger)中。

1.2.5. 性能角度审视Java异常

  • try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
  • Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。

1.2.6. finally是不是一定会执行

不一定。

  • 但是只有在try或者catch中调用退出JVM的相关方法(即System.exit(1);),此时finally才不会执行,否则finally永远会执行 。
  • finally代码块的return会冲掉之前的return。

1.2.7. final、finally、finalize区别

  • final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。

    //变量不可以修改
    final int a = 0;
    //a = 11;//不能被赋值
    System.out.println(a);
    
    //final仅约束了strList引用不能被赋值,但是strList本身可变。
    final List<String> strList = new ArrayList<>();
    strList.add("hello");
    strList.add("world");
    System.out.println(strList);
    //strList =  Arrays.asList("xxx","yyy","zzz");//不能被赋值
    
  • finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。

  • finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为deprecated。

    • Java 平台目前在逐步使用java.lang.ref.Cleaner来替换掉原有的 finalize 实现。

1.3. 面向对象

1.3.1. 面向对象可以解释下吗?都有哪些特性?

面向对象是一种思想,可以将复杂问题简单化,让我们从执行者变为了指挥者。面向对象的三大特性为:封装,继承与多态。

  • 封装:将事物封装成一个类,减少耦合,隐藏细节。保留特定的接口与外界联系,当接口内部发生改变时,不会影响外部调用方。
  • 继承:从一个已知的类中派生出一个新的类,新类可以拥有已知类的行为和属性,并且可以通过覆盖/重写来增强已知类的能力。
  • 多态:多态的本质就是一个程序中存在多个同名的不同方,主要通过三种方式来实现:
    • 通过子类对父类的覆盖来实现
    • 通过在一个类中对方法的重载来实现
    • 通过将子类对象作为父类对象使用来实现

1.3.2. 重载和重写的区别?

  • 重载指方法名称相同,但参数类型个数不同,是行为水平方向不同实现。对编译器来说,方法名称和参数列表组成了一个唯一键,称为方法签名,JVM 通过方法签名决定调用哪种重载方法。不管继承关系如何复杂,重载在编译时可以根据规则知道调用哪种目标方法,因此属于静态绑定。
    • JVM 在重载方法中选择合适方法的顺序:① 精确匹配。② 基本数据类型自动转换成更大表示范围。③ 自动拆箱与装箱。④ 子类向上转型。⑤ 可变参数。
  • 重写:指子类实现接口或继承父类时,保持方法签名完全相同,实现不同方法体,是行为垂直方向不同实现。元空间有一个方法表保存方法信息,如果子类重写了父类的方法,则方法表中的方法引用会指向子类实现。父类引用执行子类方法时无法调用子类存在而父类不存在的方法。重写方法访问权限不能变小,返回类型和抛出的异常类型不能变大,必须加 @Override

1.3.3. Object 类有哪些方法?

  • equals:检测对象是否相等,默认使用 == 比较对象引用,可以重写 equals 方法自定义比较规则。equals 方法规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null) 返回 false。
  • hashCode:散列码是由对象导出的一个整型值,没有规律,每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,一般需要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同,因此 hashCode 是对象相等的必要不充分条件。
  • toString:打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。
  • clone:clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。
  • finalize:确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链上的对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序,在 JDK 9 被标记为过时方法,并不适合释放资源。
  • getClass:返回包含对象信息的类对象。
  • wait / notify / notifyAll:阻塞或唤醒持有该对象锁的线程。

1.3.4. 内部类的作用是什么,有哪些分类?

内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。

  • 静态内部类: 属于外部类,只加载一次。作用域仅在包内,可通过 外部类名.内部类名 直接访问,类内只能访问外部类所有静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类。内部类中还可以定义内部类,如 ThreadLoacl 静态内部类 ThreadLoaclMap 中定义了内部类 Entry。
  • 成员内部类: 属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。
  • 局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。
  • 匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。

1.4. 数据类型

1.4.1. Java 有哪些基本数据类型?

数据类型 内存大小 默认值 取值范围
byte 1 bit (byte)0 -128 ~ 127
short 2 bit (short)0 -2^15^ ~ 2^15^-1
int 4 bit 0 -2^31^ ~ 2^31^-1
long 8 bit 0L -2^63^ ~ 2^63^-1
float 4 bit 0.0F ±3.4E+38(有效位数 6~7 位)
double 8 bit 0.0D ±1.7E+308(有效位数 15 位)
char 英文 1B,中文 UTF-8 占 3B,GBK 占 2B。 '\u0000' '\u0000' ~ '\uFFFF'
boolean 单个变量 4B / 数组 1B false true、false

JVM 没有 boolean 赋值的专用字节码指令,boolean f = false 就是使用 ICONST_0 即常数 0 赋值。单个 boolean 变量用 int 代替,boolean 数组会编码成 byte 数组。

1.4.2. 自动装箱/拆箱是什么?

  • 每个基本数据类型都对应一个包装类。
  • 自动装箱:将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。
  • 自动拆箱:将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
  • 比较两个包装类数值要用 equals ,而不能用 ==

1.4.3. String 是不可变类为什么值可以修改?

String 类和其存储数据的成员变量 value 字节数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。

1.4.4. 字符串拼接的方式有哪些?

  1. 直接用 + ,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 + 拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。
  2. 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +
  3. 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。

1.4.5. String、StringBuffer、StringBuilder有什么区别?

  • String :提供了构造和管理字符串的各种基本逻辑。它是一个不可变类型,因此,每次拼接、裁剪字符串时会产生新的String对象,这个效率很低。
  • StringBuffer:为了解决拼接产生太多中间对象的问题而提供的一个类,可以使用append或者add方法,将字符串添加到已有序列的末尾或者指定位置。
    • 它本质上是一个线程安全的可修改字符串的序列。(使用synchronized关键字,例如public synchronized int length()
    • 除非有线程安全的要求,否则推荐使用StringBuilder
  • StringBuilder:同StringBuffer一样,解决对象拼接问题,但是去掉了线程安全的部分,减少了开销。
public String implicit(String[] nums){
    String result = "";
    // 每次都会产生一个新的StringBuilder对象用于拼接
    for (String num : nums) {
        result += num;
    }
    return result;
}
public String explicit(String[] nums){
    StringBuilder result = new StringBuilder();
    // 仅产生一个新的StringBuilder对象用于拼接
    for (String num : nums) {
        result.append(num);
    }
    return result.toString();
}

1.4.6. String s1 = new String("abc");这句话创建了几个对象?

String s1 = new String("abc");// 堆内存的地值值
String s2 = "abc";
System.out.println(s1 == s2);// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出true

创建了两个对象。先有字符串"abc"放入常量池,然后 new 了一份字符串"abc"放入Java堆(字符串常量"abc"在编译期就已经确定放入常量池,而 Java 堆上的"abc"是在运行期初始化阶段才确定),然后 Java 栈的 str1 指向Java堆上的"abc"。


1.5. 引用

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

不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。

  • 强引用:这是最常见的对象引用。只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会回收它。

    • String strongReference = new String("abc"); //强引用

    • GC可能会回收强引用的情况,具体回收时机还是要看GC策略。

      1. 超过了引用的作用域。

        在一个方法的内部有一个强引用,这个引用保存在Java中,而真正的引用内容(Object)保存在Java中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数0,这个对象会被回收。

      public void test() {
          Object strongReference = new Object();
          // 省略其他操作
      }
      
      1. 显式地将相应强引用赋值为 null,在ArrayList类中定义了一个elementData数组,在调用clear方法清空数组时,每个数组元素被赋值为null
  • 软引用:它是相对强引用弱化一些的引用。只有当 JVM 认为内存不足OutOfMemoryError时,才会去试图回收软引用指向的对象。

    // 软引用
    String str = new String("abc");
    SoftReference<String> softReference = new SoftReference<String>(str);
    
    • 它可用来实现内存敏感的高速缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
    • 当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:
    if(JVM内存不足) {
        // 将软引用中的对象引用置为null
        str = null;
        // 通知垃圾回收器进行回收
        System.gc();
    }
    
    • 应用场景:浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?

      1. 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
      2. 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。

      这时候就可以使用软引用,很好的解决了实际的问题:

    // 获取浏览器对象进行浏览
    Browser browser = new Browser();
    // 从后台程序加载浏览页面
    BrowserPage page = browser.getPage();
    // 将浏览完毕的页面置为软引用
    SoftReference softReference = new SoftReference(page);
    // 回退或者再次浏览此页面时
    if(softReference.get() != null) {
        // 内存充足,还没有被回收器回收,直接获取缓存
        page = softReference.get();
    } else {
        // 内存不足,软引用的对象已经回收
        page = browser.getPage();
        // 重新构建软引用
        softReference = new SoftReference(page);
    }
    
  • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

    String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    str = null;
    
    • 它可以用来构建一种没有特定约束的关系。例如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。
    • 实现缓存的方式。
  • 虚引用:你不能通过它访问对象。

    • 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
    • 用于监控对象的创建和销毁。



1.6. 反射机制

反射机制:将类的各个组成部分封装成其他对象。

  • 获取Class对象的方式:
    1. Class.forName("全类名”):将字节码文件加载进内存,返回Class对象
    2. 类名.Class:通过类名的属性class获取。
    3. 对象.getClass():getClass()方法在Object类钟定义。
  • 获取构造方法:
    1. Constructir<?>[] getConstructors()
    2. Constructir<?> getConstructors(类<?> ...parameterTypes)

反射机制可以让我们在编译期不用知道某个对象的类型,而在运行期直接操作类(获得任何一个类的字节码),包括接口、变量、方法等信息。还可以让我们在运行期实例化对象,通过调用get/set方法获取变量的值。

参考1:简单理解反射机制

1.7. 动态代理

1.7.1. 代理模式

代理模式属于结构型模式,为其他对象提供一种代理以控制对这个对象的访问。优点是可以增强目标对象的功能,降低代码耦合度,扩展性好。缺点是在客户端和目标对象之间增加代理对象会导致请求处理速度变慢,增加系统复杂度。

Spring 利用动态代理实现 AOP,如果 Bean 实现了接口就使用 JDK 代理,否则使用 CGLib 代理。

静态代理:代理对象持有被代理对象的引用,调用代理对象方法时也会调用被代理对象的方法,但是会在被代理对象方法的前后增加其他逻辑。需要手动完成,在程序运行前就已经存在代理类的字节码文件,代理类和被代理类的关系在运行前就已经确定了。 缺点是一个代理类只能为一个目标服务,如果要服务多种类型会增加工作量。

动态代理:动态代理在程序运行时通过反射创建具体的代理类,代理类和被代理类的关系在运行前是不确定的。动态代理的适用性更强,主要分为 JDK 动态代理和 CGLib 动态代理。

  • JDK 动态代理:通过 Proxy 类的 newInstance 方法获取一个动态代理对象,需要传入三个参数,被代理对象的类加载器、被代理对象实现的接口,以及一个 InvocationHandler 调用处理器来指明具体的逻辑,相比静态代理的优势是接口中声明的所有方法都被转移到 InvocationHandlerinvoke 方法集中处理。
    • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
    • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
    • 代码实现简单。
  • CGLib 动态代理:JDK 动态代理要求实现被代理对象的接口,而 CGLib 要求继承被代理对象,如果一个类是 final 类则不能使用 CGLib 代理。两种代理都在运行期生成字节码,JDK 动态代理直接写字节码,而 CGLib 动态代理使用 ASM 框架写字节码,ASM 的目的是生成、转换和分析以字节数组表示的已编译 Java 类。 JDK 动态代理调用代理方法通过反射机制实现,而 GCLib 动态代理通过 FastClass 机制直接调用方法,它为代理类和被代理类各生成一个类,该类为代理类和被代理类的方法分配一个 int 参数,调用方法时可以直接定位,因此调用效率更高。
    • 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 cglib 动态代理就没有这种限制。
    • 只操作我们关心的类,而不必为其他相关类增加工作量。
    • 高性能。

1.7.2. 动态代理基于什么原理?

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都利用类似的机制做到的,例如面向切片编程(AOP)、包装RPC调用。

实际上,代理可以理解成对调用对象目标的一个包装。

我们对目标代码的调用不是直接发生的,而是通过代理完成的。这样做的好处是实现了调用者和实现者之间的解耦。例如RPC调用中的框架内部的寻址、序列化、反序列化等。实现动态代理的方式有很多。

  1. JDK自身提供的,它主要利用了反射机制;
  2. 高性能的字节码操作机制,类似于ASM,cglob(基于ASM)、Javassist等。

1.7.3. 谈谈Spring AOP技术

它是对OOP的一种补充,解决了OOP对于跨越不同对象或类的分散、纠缠逻辑表现力不够。如图所示,不同模块的特定阶段要完成通用日志处理、安全策略、事务框架等任务。

使用AOP通过动态代理机制,可以让开发者简化开发流程,大幅度提高代码的抽象程度和复用度。


1.8. 基本数据类型与包装类

1.8.1. int与Integer有什么区别?

  • int是JAVA的8个原始基本类型之一;
  • Integerint的包装类,其中含有int类型的字段储存数据,并提供了基本操作(数学运算、字符串转换等)。在JAVA5后,引入了自动装箱和自动拆箱的功能,可以根据上下文,自动进行切换。

1.8.2. 谈谈Integer的值缓存范围

  • Integer缓存:
    • 传统:Integer x =new Integer(9);创建的Integer对象(栈内存)
    • 如今:Integer x =Integer.valueOf(9);从内存池中获取对象(方法区内存池)
  • 默认值:-128到127

1.8.3. 自动装箱与拆箱

  • 自动装箱:Integer.valueOf()
  • 自动拆箱:Integer.intValue()

1.8.4. 其他包装类也会使用这样的缓存方式

1.8.5. 原始类型线程安全

  • 原始数据类型的变量:需要使用并发满足线程安全。
    • AtomicInteger、AtomicLong线程安全类
  • 比较宽的数据类型不能保证更新操作的原子性。

1.8.6. equals与"=="区别

== equals
基本数据类型 比较值 比较值
引用数据类型 比较地址 根据重写方法定,默认(比较地址)。

1.8.7. java基本数据类型

byte(8位)、short(16位)、int(32位)、long(64位)、float(32位)、double(64位)、char(16位)、boolean(8位)

1.8.8. 基本数据类型存储在哪?引用数据类型存储在哪?

  • 基本数据类型的存储原理:所有的简单数据类型不存在“引用”的概念,基本数据类型都是直接存储在内存中的内存栈上的,数据本身的值就是存储在栈空间里面,Java语言里面八种数据类型是这种存储模型;
  • 引用类型的存储原理:引用类型继承于Object类(也是引用类型)都是按照Java里面存储对象的内存模型来进行数据存储的,使用Java内存堆和内存栈来进行这种类型的数据存储。简单地讲,“引用”(存储对象在内存堆上的地址)是存储在有序的内存栈上的,而对象本身的值存储在内存堆上的;

1.9. 基本运算

1.9.1. 原码/反码/补码

public static void main(String[] args) {
    byte a = 127;
    byte b = 127;
    a+=b;
    System.out.println(a);//-2
}

输出为-2。

byte 数据类型是8位、有符号的,以二进制补码表示的整数,最小值是-128,最大值是127。

下面讲解一下 原码反码补码,参考这篇文章。在计算机中,最高位用于存放符号,正数代表0,负数代表1。为了解决计算问题(减法会出错),使用补码相加。

1-1=1+(-1)=[00000001]原+[10000001]原=[10000010]原=-2
1-1=1+(-1)=[0000 0001]原+[1000 0001]原=[0000 0001]补+[1111 1111]补=[0000 0000]补=[0000 0000]原
  • 原码:符号位加上真值的绝对值,即用第一位表示符号, 其余位表示值。

  • 反码:正数的反码是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。

  • 补码:正数的补码就是其本身;负数的补码是在反码的基础上+1。

原码:
    [+1]原 = 0000 0001
    [-1]原 = 1000 0001
反码:
    [+1] = [0000 0001]原 = [0000 0001]反
    [-1] = [1000 0001]原 = [1111 1110]反
补码:
    [+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
    [-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补

本题中,127 的补码是0111 1111,

127+127=[0111 1111]补+[0111 1111]补=[1111 1110]补=[1111 1101]反=[1000 0010]原=-2

1.10. 关键字

1.10.1. final

1. 数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

  • 对于基本类型,final 使数值不变;
  • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final int x = 1;
// x = 2;  // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;

2. 方法

声明方法不能被子类重写。

private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

3. 类

声明类不允许被继承。

为什么将一个对象指明为 final ?1. 它可以防止方法被重写。2. 它有效地”关闭了“动态绑定,或者说告诉编译器不需要对其进行动态绑定。这可以让编译器为 final 方法生成更高效的代码。但是,大部分情况下这样做不会对程序的整体性能带来什么改变,因此最好是为了设计使用 final,而不是为了提升性能而使用。

1.10.2. 如何实现 immutable 的类。

  • 将 class 自身声明为 final,这样别人就不能扩展来绕过限制了
  • 将所有成员变量定义为 private 和 final,并且不要实现 setter 方法。
  • 通常构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改。
  • 如果确实需要实现 getter 方法,或者其他可能会返回内部状态的方法,使用 copy-on-write原则,创建私有的 copy。
final List<String> strList = new ArrayList<>();
strList.add("Hello");
strList.add("world");//可以添加
List<String> unmodifiableStrList = Arrays.asList("hello", "world"); // 底层 初始化一个final 数组
unmodifiableStrList.add("again");//UnsupportedOperationException

1.10.3. static

1. 静态变量

  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
public class A {

    private int x;         // 实例变量
    private static int y;  // 静态变量

    public static void main(String[] args) {
        A a = new A();
        int x = a.x;
        int y = A.y;
    }
}

2. 静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

public abstract class A {
    public static void func1(){
    }
}

访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。

public class A {

    private static int x;
    private int y;

    public static void func1(){
        int a = x;
        // int b = y;  // Non-static field 'y' cannot be referenced from a static context
        // int b = this.y;     // 'A.this' cannot be referenced from a static context
    }
}

3. 静态语句块

静态语句块在类初始化时运行一次。

public class A {
    static {
        System.out.println("123");
    }

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}
123

4. 静态内部类

非静态内部类依赖于外部类的实例,而静态内部类不需要。

public class OuterClass {

    class InnerClass {
    }

    static class StaticInnerClass {
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}

静态内部类不能访问外部类的非静态的变量和方法。

5. 静态导包

在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

import static com.xxx.ClassName.*

6. 初始化顺序

静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

public static String staticField = "静态变量";
static {
    System.out.println("静态语句块");
}
public String field = "实例变量";
{
    System.out.println("普通语句块");
}

最后才是构造函数的初始化。

public InitialOrderTest() {
    System.out.println("构造函数");
}

存在继承的情况下,初始化顺序为:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

1.10.4. this

只能在方法内部使用,表示对“调用该方法的对象”的引用。

1.11. 初始化与清理

1.11.1. 成员初始化

局部变量必须进行初始化,不然编译错误;成员变量可以不进行初始化,会进行默认初始化。

class Test {
    private int a;
    public Test() {
        System.out.println(a); // 没问题
    }
    public void say(){
        int i;
        System.out.println(i);  // 报错
    }
    public static void main(String[] args) {
        Test test = new Test();
        test.say();
    }
}

1.11.2. 构造器初始化

  • 在类的内部,变量定义的先后顺序决定了初始化的顺序;此外,它们都会在任何方法(包括构造器)被调用之前得到初始化。

    public class Test {
        T t = new T("1");
        public Test() {
            System.out.println("construction");
        }
        T t1 = new T("2");
        public static void main(String[] args) {
            Test test = new Test();
        }
    }
    class T {
        public T(String name) {
            System.out.println(name);
        }
    }
    //1
    //2
    //construction
    
  • 静态变量:该对象首次被初始化后,进行初始化。此后,不再能被初始化。

  • 初始化顺序:先静态变量,再非静态变量。

1.12. 继承

1.12.1. 初始化

初始化子类时,也会同时初始化父类。

public class Test extends A {
    public Test() { System.out.println("Test"); }
    public static void main(String[] args) {
        Test test = new Test();
    }
}
class A {
    public A() { System.out.println("A"); }
}
// A
// Test

为了继承,一般是将所有数据成员指定为private,将所有的方法指定为public

1.12.2. 向上转型

将子类引用转换为父类引用的动作,称为向上转型。它是很安全的,导出类是基类的一个超集,它可能比基类含有更多的方法,但它必须至少具备基类中所含的方法。

1.12.3. 抽象类与接口

1. 抽象类

  • 抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
  • 抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。

2. 接口

  • 接口是抽象类的延伸,在 Java 8 之前,它可以看成是 完全抽象的类(即没有任何的方法实现)。
  • 从 Java 8 开始,接口也可以拥有默认的方法实现。
  • 接口的成员(属性和方法)默认都是 public 的。
  • 接口的属性默认都是 static 和 final 的。
public interface InterfaceExample {
    void func1();
    default void func2(){ System.out.println("func2"); }
    int x = 123;
    // int y;               // Variable 'y' might not have been initialized
    public int z = 0;       // Modifier 'public' is redundant for interface fields
    // private int k = 0;   // Modifier 'private' not allowed here
    // protected int l = 0; // Modifier 'protected' not allowed here
    // private void fun3(); // Modifier 'private' not allowed here
}

3. 比较

  • 设计上:
    • 抽象类:IS-A 关系,那么就必须满足里式替换原则(不能替换父类的方法)。
    • 接口:LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  • 使用上:
    • 类可以实现多个接口,但是不能继承多个抽象类。
    • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
    • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

4. 使用选择

  • 使用接口:
    • 需要实现所有方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
    • 需要使用多重继承。
  • 使用抽象类:
    • 需要在几个相关的类中共享代码。
    • 需要能控制继承来的成员的访问权限,而不是都为 public。
    • 需要继承非静态和非常量字段。

1.13. 多态

1.13.1. 方法调用绑定

  • 绑定:将一个方法调用同一个方法主题关联起来。
  • 前期绑定:程序执行前进行绑定。在Java中只有Static和final方法称为前期绑定,以防止动态绑定。
  • 后期绑定:运行时根据对象的类型进行绑定。

1.14. IO流

同步/异步/阻塞/非阻塞 IO 的区别?

  1. 同步和异步是通信机制,阻塞和非阻塞是调用状态。
  2. 同步 IO 是用户线程发起 IO 请求后需要等待或轮询内核 IO 操作完成后才能继续执行。
  3. 异步 IO 是用户线程发起 IO 请求后可以继续执行,当内核 IO 操作完成后会通知用户线程,或调用用户线程注册的回调函数。
  4. 阻塞 IO 是 IO 操作需要彻底完成后才能返回用户空间 。
  5. 非阻塞 IO 是 IO 操作调用后立即返回一个状态值,无需等 IO 操作彻底完成。

1.14.1. 什么是 BIO?

BIO 是同步阻塞式 IO,JDK1.4 之前的 IO 模型。服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程,如果这个连接不做任何事会造成不必要的线程开销。可以通过线程池改善,这种 IO 称为伪异步 IO。适用连接数目少且服务器资源多的场景。

1.14.2. 什么是 NIO?

NIO 是 JDK1.4 引入的同步非阻塞 IO。服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接有 IO 请求时才启动一个线程处理。适用连接数目多且连接时间短的场景。

同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。

核心组件:

  • Selector: 多路复用器,轮询检查多个 Channel 的状态,判断注册事件是否发生,即判断 Channel 是否处于可读或可写状态。使用前需要将 Channel 注册到 Selector,注册后会得到一个 SelectionKey,通过 SelectionKey 获取 Channel 和 Selector 相关信息。

  • Channel: 双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要通过 Buffer 来读写数据,也可以和其他 Channel 交互。

  • Buffer: 缓冲区,本质是一块可读写数据的内存,用来简化数据读写。Buffer 三个重要属性:position 下次读写数据的位置,limit 本次读写的极限位置,capacity 最大容量。

    • flip 将写转为读,底层实现原理把 position 置 0,并把 limit 设为当前的 position 值。
    • clear 将读转为写模式(用于读完全部数据的情况,把 position 置 0,limit 设为 capacity)。
    • compact 将读转为写模式(用于存在未读数据的情况,让 position 指向未读数据的下一个)。
    • 通道方向和 Buffer 方向相反,读数据相当于向 Buffer 写,写数据相当于从 Buffer 读。

    使用步骤:向 Buffer 写数据,调用 flip 方法转为读模式,从 Buffer 中读数据,调用 clear 或 compact 方法清空缓冲区。


1.14.3. 什么是 AIO?

AIO 是 JDK7 引入的异步非阻塞 IO。服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据。适用连接数目多且连接时间长的场景。

异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。

实现方式包括通过 Future 的 get 方法进行阻塞式调用以及实现 CompletionHandler 接口,重写请求成功的回调方法 completed 和请求失败回调方法 failed


1.14.4. java.io 包下有哪些流?

主要分为字符流和字节流,字符流一般用于文本文件,字节流一般用于图像或其他文件。

字符流包括了字符输入流 Reader 和字符输出流 Writer,字节流包括了字节输入流 InputStream 和字节输出流 OutputStream。字符流和字节流都有对应的缓冲流,字节流也可以包装为字符流,缓冲流带有一个 8KB 的缓冲数组,可以提高流的读写效率。除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流 ByteArrayInputStream、文件流 FileInputStream 等。


1.14.5. 序列化和反序列化是什么?

Java 对象 JVM 退出时会全部销毁,如果需要将对象及状态持久化,就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化。

常见的序列化有三种:

  • Java 原生序列化

    实现 Serializabale 标记接口,Java 序列化保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据,兼容性最好,但不支持跨语言,性能一般。序列化和反序列化必须保持序列化 ID 的一致,一般使用 private static final long serialVersionUID 定义序列化 ID,如果不设置编译器会根据类的内部实现自动生成该值。如果是兼容升级不应该修改序列化 ID,防止出错,如果是不兼容升级则需要修改。

  • Hessian 序列化

    Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java 对象序列化的二进制流可以被其它语言反序列化。Hessian 协议的特性:① 自描述序列化类型,不依赖外部描述文件,用一个字节表示常用基础类型,极大缩短二进制流。② 语言无关,支持脚本语言。③ 协议简单,比 Java 原生序列化高效。Hessian 会把复杂对象所有属性存储在一个 Map 中序列化,当父类和子类存在同名成员变量时会先序列化子类再序列化父类,因此子类值会被父类覆盖。

  • JSON 序列化

    JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。

序列化通常会使用网络传输对象,而对象中往往有敏感数据,容易遭受攻击,Jackson 和 fastjson 等都出现过反序列化漏洞,因此不需要进行序列化的敏感属性传输时应加上 transient 关键字。transient 的作用就是把变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值。

results matching ""

    No results matching ""