vlambda博客
学习文章列表

2022大厂面试Java语法,这些你懂了吗

收录面试高频题汇总,面试复习 or 查漏补缺


Java三大特性

封装、继承、多态,Java是面向对象的。

封装

对抽象的事物抽象化成一个对象,并对其对象的属性私有化,同时提供一些能被外界访问属性的方法。

继承

从已有的类作为父类,父类派生出新的类,新的类作为子类,子类能吸收父类的数据属性和行为,并能扩展新的能力。(Java是单继承,多实现。接口可以多继承)

多态

对于同一接口,不同实例执行具有不同的表现形式的能力。

Java中多态的表现

  • 通过继承,多个子类对同一个方法的重写;
  • 通过接口,多个实现类覆盖接口的方法。

多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来

  • 动态绑定:在运行时确定的,被称为动态分派,比如方法覆盖或接口实现。
  • 另一种是静态绑定:在编译期确定的,被称为静态分派,比如方法重载;

多态的实现过程

  • 在方法调用过程中,如果子类覆盖父类的方法,则属于多态调用,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类的方法。这个过程就是方法覆盖的本质。

JDK、JRE、JVM

JDK

Java软件开发工具包,包含了Java运行环境(JVM和Java系统类库)和Java工具。

JRE

Java运行环境,目的是让计算机系统运行Java应用程序。

JVM

运行Java字节码的虚拟机,是Java实现与平台无关性的关键。

  • JVM是JRE的一部分,安装好了JRE等同于拥有了JVM。

  • JVM主要分为五个模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集。

  • 与平台无关性:”Write Once, Run Anywhere“,最初为了实现“一次编译,随处运行”的口号,Java为不同的操作系统平台实现了各自平台的虚拟机,这样Java编译后的相同的一份字节码,可以在不同的操作系统上运行出相同的结果,为开发者屏蔽了与具体平台的信息。


基本数据类型及包装类 & String

八种基本数据类型

名称 数据类型 默认值 取值范围
字节 byte 8 0 -128(-2^7) ~ 127(2^7 - 1)
短整形 short 16 0 -32768(-2^15) ~ 32767(2^15 - 1)
整形 int 32 0 -2147483648(-2^31) ~ 2147483647(2^31 - 1)
长整形 long 64 0L -9223372036854775808(-2^63)~9223372036854775807(2^63 -1)
单精度 float 32 0.0f 1.4E-45 ~ 3.4028235E38
双精度 double 64 0.0d 4.9E-324 ~ 1.7976931348623157E308
字符 char 16 /u0000 0 ~ 65535(2^16 - 1)
布尔 boolean 1 false truefalse

注意:boolean在官网未做明确定义,从逻辑上讲是占用1位,但是具体实现都依赖于各JVM厂商。

我们都知道在这八种基本数据类型中都有对应的包装类型,分别为:ByteShortIntegerLongFloatDoubleCharacterBoolean

缓存池:包装类型会为其对应的基本数据类型默认创建限定范围的缓存数据

  • ByteShortIntegerLong[-128,127]
  • Character[0,127]
  • Booleantruefalse

装箱和拆箱:基本数据类型和其包装类型语法上互相兼容使用

  • 装箱:把基本数据类型的引用包装成包装类型
  • 拆箱:把包装类型的数据转为其基本数据类型

String

字符串类型,除了八大基本数据类型外,是最常用的数据类型了。

底层是使用字符数组char[]实现,并使用final修饰,不可变类型

  • 为什么设计成不可变类型呢?
    • 非常常用,使用字符串常量池。创建相同的字符串时,引用都指向同个地址,可以复用
    • 天然不可变性具有线程安全的特点
    • HashCode值不会变化,无需重复计算


public、protect、default、private

四种访问修饰符的权限范围,访问权限范围越小,安全性越高。

访问修饰符 子类 其他包
public
protect ×
default × ×
private × × ×

final、finally、finalize

final

final是Java中的关键字,用来修饰类、方法和变量。final翻译结果为”最后的、最终的“,也可认为是敲定的,不可改变的。

  • 修饰类:当类被final修饰时,意味着该类是不会被继承的,其中该类的所有方法会隐式的定义为final方法。
  • 修饰方法:当方法被final修饰时,意味着该方法不可被重写。
  • 修饰变量:当变量被final修饰时,意味着该变量赋值一次后就不可改变了。当变量赋值的是引用类型的时候,引用类型的对象属性是可以改变的,不可改变的是该变量所指向的引用类型的这个关系。

finally

finally是Java中的关键字,用于try/catch异常处理的一部分,表示在try/catch执行之后再执行一个语句块。

同时,另一个需要注意的点是finally语句块不一定会执行,比如遇到System.exit(0)或线程interrupted时,就不会执行到finally。

finalize

finalize()是Object中定义的一个方法,也就是每个对象都有这个方法。在GC发生的时候,该方法的代码块会被执行,如果被执行该方法的对象被存活对象重新引用上了,那么该对象就不会被GC回收,也就是不会被销毁。不过,一般不推荐使用该方法。


顶级父类Object

Object是所有类的父类。Object的方法有:hashCode、clone、getClass、equals、toString、wait、notify/notifyAll、finalize


接口和抽象类区别

抽象类:使用abstract修饰的类;抽象类只能被继承,所以不能使用final修饰,抽象类不能被实例化;

接口:接口是一个抽象类型,是抽象方法的集合,接口支持多继承,接口中定义的方法,默认是public abstract修饰的抽象方法

相同点:

  • 抽象类和接口都不能被实例化
  • 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法

不同点:

  • 抽象类有构造方法,接口没有构造方法
  • 抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以)
  • 抽象类只能单继承,接口可以多继承
  • 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量

位运算

二进制位的计算

符号 描述 运算规则 示例
& 两个二进制位都为1时,结果为1,否则为0 1 & 1 = 1
| 两个二进制位都为0时,结果为0,否则为1 0 | 0 = 0
^ 异或 两个二进制位相同为0,不同为1 0 ^ 0 = 0
~ 取反 二进制位,0变1,1变0 ~0 = 1
<< 左移 二进制位全部左移若干位,高位丢弃,低位补0 1011 0011 << 2 = 1100 1100
>> 右移 二进制位全部右移若干位,低位丢弃,正数高位补0,负数高位补1 1011 0011 >> 2 = 11101100
>>> 无符号右移 无符号右移,也叫逻辑右移,低位丢弃,正负数高位都补0 1011 0011 >>> 2 = 0010 1100

hashcode、equals 和 ==

hashcode

对于集合来说,想要快速判断或检索集合中的元素,如果要每个元素都要做比对,同时每个元素属性复杂,那么性能是会出现瓶颈,效率是不够的。于是提出了hashcode值,hashcode()是Object的一个方法,也就是每个对象都有一个hashcode值,在集合中处理元素时,可以先通过hashcode值快速定位或判断集合中的元素,当hashcode值相同时,再equals方法比对,从而达到高效的目的。这也是相同hashcode值不一定是相同对象,相同对象一定hashcode值相同的说法。

equals

Object的一个方法,也就是每个对象都有个equals方法,用于比较两个对象是否相等,通过重写该方法用于判断定义的两个对象是否相等,比较常见的是String类中实现的equals方法。

==


值传递

对值传递(pass by value)定义:在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

在Java中只有值传递。

如果向方法传递的参数是基本数据类型,那么就是拷贝一份相同的参数传递给下个方法,在下个方法里对这个参数修改是不会影响原传递的方法中的参数值;

如果向方法传递的参数是对象的引用类型,那么就会拷贝一份相同对象引用传递给下个方法,在下个方法里对这个对象进行修改,那么是会影响到原来对象的,因为是修改是同个对象。


泛型

泛型的本质是参数化类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

泛型擦除:

Java的泛型是伪泛型,使用泛型的时候加上类型参数,在编译器编译生成的字节码的时候会去掉,这个过程成为类型擦除。如List<String>等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的,可以通过反射添加其它类型元素。


反射

在Java运行状态中,对于任意一个类都能够获取这个类所有的属性和方法;并且都能够调用它的任意一个方法;

原理

反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成相应的 Method、Filed、Constructor 等类

反射的优缺点

优点:使代码灵活多变,扩展性强,复用性高

缺点:安全问题、性能差些、使用不当出现错误


静态/动态代理

静态代理

需要手动创建class去生成动态代理类,在编译期就确定代理什么类。

动态代理

运行时根据反射生成目标代理类,在运行时才确定代理什么类

jdk动态代理

只有实现接口才能代理,性能差些

  • 原理:生成的目标代理类继承Proxy类和实现了代理类的接口,并且持有InvocationHandler实例,这样在调用代理类方法的时候,就会调用InvocationHandler实例的invoker方法
  • 流程:Proxy.newProxyInstance(ClassLoader,Class<?>[], InvocationHandler)
    • 对InvocationHandler做空判断
    • 复制代理类接口
    • 通过代理类接口的类加载器生成目标代理类,其中目标代理的类的构造方法会传入InvocationHandler实例
    • 获取目标代理类构造器
    • 通过构造器实力化目标代理类

cglib动态代理

通过ASM技术去继承需要代理的类,以实现动态代理,但是不能代理final修饰的类或方法

javasisxt动态代理

通过api拼接class代码生成字节码,实现动态代理类,性能较好

ASM技术动态代理

使用ASM技术直接修改字节码,实现动态代理,性能比上者好,但是需要人工手动操作且麻烦


克隆(深拷贝/浅拷贝)

浅拷贝:创建一个与原对象相同class的新对象,如果对象的属性有基本数据类型,那么会从原对象拷贝一份到新对象;如果对象的属性是有引用类型,那么也会从原对象拷贝一份引用到新对象。

因此,浅拷贝后两个对象中的引用类型属性指的是同个对象。

深拷贝:完全创建与原对象相同的新对象,包括基本数据类型和引用类型对象都是新的。

因此,深拷贝后两个对象中引用类型属性指的不是同个对象。


序列化

序列化的目的是用于网络传输或数据存储,机器在处理二进制数据的时候效率更高。

序列化

将数据结构或对象转换为二进制字节流的过程

反序列化

将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

Java中有些字段不想序列化可以使用transient修饰。


异常体系

异常又分检查型异常和非检查型异常:

检查型异常(Checked Exception):除了RuntimeException下的所有异常都属于检查型异常,需要手动使用try/catch去处理的异常,否则编译器会报错。

非检查型异常(Unchecked Exception):RuntimeException下的所有异常,不需要手动去处理的异常,编译器可以通过。在程序运行阶段发生直接报错的异常,也可以手动去捕获这类异常。


类加载

类加载是指JVM把class类文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成JVM可以直接使用的Java类型。

类加载过程

  1. 加载(Loading)

    .class文件的二进制数据读入内存,将静态存储结构转化为方法区的运行时的数据结构,并生成一个java.lang.Class对象,作为访问该类的入口。

  2. 连接

    a. 验证(Verification):验证加载的类是否正确,是否符合JVM的要求等。

    b. 准备(Preparation):为类的变量分配内存和赋默认初值。如果是静态常量直接赋程序给定的值。

    c. 解析(Resolution):将类的二进制数据中的符号引用转换为直接引用。

  3. 初始化(Initialization)

    类的初始化操作,为类的静态变量赋程序设定的值和执行构造方法。

  4. 使用(Using)

  5. 卸载(Unloading)

JVM规范中严格规定了只有4种情况必须对类进行初始化

  • 使用new关键字,读取或设置静态字段的值或调用一个静态方法
  • 反射调用时,如果类没有初始化,必须初始化
  • 初始化一个类时,如果发现父类没有初始化,则首先触发父类初始化
  • main()方法的类,JVM启动的时候会首先初始化这个类

双亲委派模型

JVM启动并不是一次性加载所有类的,而是按需加载的。

类加载器之间存在着父子的关系(区别于Java的继承),子加载器持有父加载器的引用,子加载器会委托父加载器加载类,如果父加载器加载失败,那么会由子加载器加载。

每个类加载器都负责加载不同路径下的class类

  • Bootstrap类加载器:加载核心类库,比如rt.jar
  • Ext类加载器:加载/lib/ext路径下的类
  • App类加载器:加载classpath路径下的类

这就是双亲委派模型,它的好处是

  • 主要是为了安全,避免用户自己编写的类动态替换Java的核心类,比如String。
  • 同时,也是为了避免类的重复加载,遇到相同的类直接复用即可。

如何自定义类加载器:

  • 如果不想打破双亲委派,那么只需要重写findClass方法;
  • 如果想打破双亲委派,那么就重写整个loadClass方法。

线程上下文类加载器:

JDK1.2引入,在线程中可以直接获得线程上下文类加载器contextClassLoader,它破坏了双亲委派模型。

在Java的核心类库中,有些SPI接口,它实现类都是由第三方实现的。SPI接口中有些需要调用第三方实现的代码,但是在Bootstrap类加载器加载SPI接口时,却无法直接加载第三方实现类,同时由于双亲委派模型存在,也无法反向委托AppClassLoader加载第三方实现类。因此,Bootstrap类加载器会委托线程上下文类加载器去加载第三方实现类,线程上下文类加载器的父类加载器是AppClassLoader加载器,这样Bootstrap类加载器就可以加载到SPI接口的第三方实现类了。非常典型第三方实现的例子,比如mysql驱动包中的com.mysql.jdbc.Driver


参考资料

[1] javaguide:https://javaguide.cn/java/basis

[2] 百度百科

[3] 菜鸟教程