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面向对象的理解

面向对象主要针对面向过程,面向过程的基本单元是函数,在java语言中万物皆可以看作对象。

1.1.3. JDK与JRE区别

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

1.1.4. 存储位置

  • 寄存器:最快的存储区,Java程序不能直接控制。
  • 堆栈:用于存储对象引用和基本数据类型。
  • 堆:用于存放所有Java对象的。

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. 强引用、软引用、弱引用、幻引用有什么区别?

不同的引用类型,主要体现的是对象不同的可达性(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.4. 字符串

1.4.1. String不可变

每次修改String值的方法,实际上就是创建一个新的String对象。

1.4.2. 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.3. 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. 反射机制

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

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

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

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

1.6. 动态代理

1.6.1. 谈谈反射机制

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

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

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

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

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

1.6.3. 谈谈Spring AOP技术

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

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


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

1.7.1. int与Integer有什么区别?

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

1.7.2. 谈谈Integer的值缓存范围

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

1.7.3. 自动装箱与拆箱

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

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

1.7.5. 原始类型线程安全

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

1.7.6. equals与"=="区别

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

1.7.7. java基本数据类型

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

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

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

1.8. 基本运算

1.8.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.9. 关键字

1.9.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. 类

声明类不允许被继承。

1.9.2. 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.9.3. this

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

1.10. 初始化与清理

1.10.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.10.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.11. 继承

1.11.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.11.2. 向上转型

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

1.11.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.12. 多态

1.12.1. 方法调用绑定

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

results matching ""

    No results matching ""