基础

概述

什么是Java

Java是一种面向对象的编程语言,由Sun Microsystems(现在是Oracle Corporation)于1995年推出。Java具有跨平台性、安全性、可移植性、简单性和可靠性等特点,广泛应用于开发Web应用、移动应用、桌面应用、嵌入式系统等领域。Java还有一个重要的特点就是它支持多线程编程,这使得Java程序可以同时执行多个任务,从而提高了程序的效率和性能。

JVMJDKJRE 有什么区别?

JVM:Java Virtual Machine,Java虚拟机,Java程序运行在Java虚拟机上。针对不同系统的实现(Windows,Linux,macOS)不同的JVM,因此Java语言可以实现跨平台。

JRE: Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括Java 虚拟机(JVM),Java 类库,Java 命令和其他的⼀些基础构件。但是,它不能用于创建新程序。

JDK: Java Development Kit,它是功能⻬全的 Java SDK。它拥有 JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如 javadoc 和 jdb)。它能够创建和编译程序。

简单来说,JDK包含JRE,JRE包含JVM。

说说什么是跨平台性?原理是什么?

所谓跨平台性,是指Java语言编写的程序,一次编译后,可以在多个系统平台上运行。

实现原理:Java程序是通过Java虚拟机在系统平台上运行的,只要该系统可以安装相应的Java虚拟机,该系统就可以运行java程序。

什么是字节码?采用字节码的好处是什么?

所谓的字节码,就是Java程序经过编译之类产生的.class文件,字节码能够被虚拟机识别,从而实现Java程序的跨平台性。

Java 程序从源代码到运行主要有三步:

  • 编译 :将我们的代码(.java)编译成虚拟机可以识别理解的字节码(.class)
  • 解释 :虚拟机执行Java字节码,将字节码翻译成机器能识别的机器码
  • 执行 :对应的机器执行二进制机器码

只需要把Java程序编译成Java虚拟机能识别的Java字节码,不同的平台安装对应的Java虚拟机,这样就可以可以实现Java语言的平台无关性。

基础语法

8种基本数据类型

基本数据类型

使用基本数据类型的好处

在Java中,新创建的对象都是储存在堆里的(不考虑JIT优化的情况下),通过栈中的引用来使用这些对象。所以对象本身是比较消耗资源的。

对于经常用的类型,如int等,如果每次使用这种变量时都新建一个对象,就会比较笨重。所以Java提供了基本数据类型,不需要使用new创建,直接在栈内存中存储,因此会更加高效。

整型的取值范围

Java中的整型属于有符号数。

8bit可以表示的数字:

  • 最大值:10000000(-128)
  • 最小值:01111111(127)

1字节=8位(bit)。

类型 字节 范围 默认值
byte 1 -128($-2^7$)— 127($2^7-1$) 0
short 2 -32768($-2^{15}$)— 32767($2^{15}-1$) 0
int 4 -2147483648($-2^{31}$)— 2147483647($2^{31}-1$) 0
long 8 -922372036854775805($-2^{63}$)— 9223372036854775807($2^{63}-1$) 0L或0l
  • float:单精度,4字节,32位。
  • double:双精度,8字节,64位。
  • boolean:被编译成 int 类型来使用,1个字节(8个比特) 。
  • char:2字节,Java编译器默认使用Unicode编码。

自动类型转换、强制类型转换?看看这几行代码?

Java 所有的数值型变量可以相互转换,当把一个表数范围小的数值或变量直接赋给另一个表数范围大的变量时,可以进行自动类型转换;反之,需要强制转换。

这就好像,小杯里的水倒进大杯没问题,但大杯的水倒进小杯就不行了,可能会溢出。

float f=3.4 ,对吗?

不正确。3.4 是单精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f =(float)3.4; 或者写成 float f =3.4F

short s1 = 1; s1 = s1 + 1; 对吗? short s1 = 1; s1 += 1; 对吗?

对于 short s1 = 1; s1 = s1 + 1;编译出错,由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。

而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。

自动拆装箱

  • 自动装箱:将基本数据类型自动转换成对应的包装类。通过包装类的valueof()方法实现。
  • 自动拆箱:将包装类自动转换成对应的基本数据类型。通过包装类对象的xxxValue()方法实现。
基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

整型的缓存机制

在Java5中,在Integer的操作上引入了一个新功能来节省内存和提高性能——整型对象通过使用相同的对象引用实现了缓存和重用。

  • 适用于-128至127区间的整数值。

  • 只适用于自动装箱。使用构造函数创建的对象不适用。

在Java6中,可以通过java.lang.Integer.IntegerCache.high设置最大值。

其他缓存对象:

  • ByteCache用于缓存Byte对象。
  • ShortCache用于缓存Short对象。
  • LongCache用于缓存Long对象。
  • CharacterCache用于缓存Character对象。

Byte、Short、Long对象都有固定的数值范围:-128至127。Character对象的数值范围是0至127。

除了Integer对象,数值范围都不能改变。

基本类型和包装类怎么选

  • 所以的POJO类属性必须使用包装数据类型。
  • RPC方法的返回值和参数必须使用包装数据类型。
  • 所有的局部变量使用基本数据类型。

包装类的默认值都是null,而基本数据类型的默认值是一个固定值,比如boolean是false,int是0等。

如果返回值使用基本数据类型,出现异常,则可能返回的是一个默认值。系统异常可能无法感知。

**&&&**有什么区别?

&和&&都是逻辑与运算符。

&是按位与运算符,而&&是短路与运算符。

  • &会对两个操作数进行按位与运算,然后返回一个结果,无论第一个操作数的值是true还是false,都会对第二个操作数进行运算。

  • &&只有在第一个操作数的值为true时才会对第二个操作数进行运算,如果第一个操作数的值为false,那么就不会对第二个操作数进行运算,这就是所谓的短路效应。

    例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为

    1
    username != null && !username.equals("") 

    二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。

switch 是否能作用在 byte/long/String上?

Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。

从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。

从 Java 7 开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中

都是不可以的。

break ,continue ,return 的区别及作用?

  • break 跳出整个循环,不再执行循环( 结束当前的循环体 )

  • continue 跳出本次循环,继续执行下次循环( 结束正在执行的循环 进入下一个循环条件 )

  • return 程序返回,不再执行下面的代码( 结束当前的方法 直接返回 )

用最有效率的方法计算2乘以8

2 << 3。位运算,数字的二进制位左移三位相当于乘以2的三次方。

说说自增自减运算?

当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。

例如,当 b = ++a 时,先自增(自己增加 1),再赋值(赋值给 b);当 b =a++ 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。

用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。

面向对象

面向对象和面向过程的区别?

  • 面向过程 :面向过程是一种基于任务的编程方式,强调的是算法、流程和数据结构,将问题分解为一系列的步骤,通过函数或者子程序的方式来实现。面向过程的程序设计风格类似于流水线式的生产方式,即将数据从一个处理单元传递到另一个处理单元,每个处理单元都是一个函数或者子程序。

  • 面向对象 :面向对象是一种基于对象的编程方式,强调的是对象和类的概念,将问题分解为一系列的对象,通过对象之间的交互来实现。面向对象的程序设计风格类似于组装式的生产方式,即将各个组件组装成一个整体,每个组件都是一个对象或者类。

面向对象的编程方式具有封装性、继承性、多态性等特点,可以提高代码的可维护性、可重用性和可扩展性,适用于大型复杂系统的开发。而面向过程的编程方式则适用于简单的问题解决,代码简单易懂,执行效率高。

面向对象有哪些特性?

  • 封装

    将数据和方法封装在一个类中,通过访问控制符来保护数据的安全性,避免数据被外部直接访问或修改,从而提高代码的可维护性、安全性和可重用性。

  • 继承

    通过继承机制,一个类可以从另一个类中继承属性和方法,来实现代码的复用和扩展,减少代码的重复编写,提高代码的可读性和可维护性。

  • 多态

    同一个方法可以在不同的对象上产生不同的行为,即一个方法可以有多种不同的实现方式,这种特性称为多态。多态可以提高代码的灵活性和扩展性,使得程序更加易于扩展和维护。

    在Java中实现多态需要满足以下两个条件

    1. 继承:多态是基于继承机制实现的,即子类可以继承父类的属性和方法。
    2. 重写:子类重写(Override)父类的方法,即子类可以对父类的方法进行重新实现,从而实现多态。

    具体实现步骤如下:

    1. 定义一个父类,其中包含一个方法。
    2. 定义一个子类,继承自父类,并重写父类的方法。
    3. 创建一个父类的对象,并将其指向子类的实例,即父类引用指向子类对象。
    4. 调用父类的方法,由于子类重写了该方法,因此会调用子类的方法,实现多态。

    示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Animal {
    public void move() {
    System.out.println("动物可以移动");
    }
    }

    class Dog extends Animal {
    public void move() {
    System.out.println("狗可以跑和走");
    }
    }

    public class Test {
    public static void main(String[] args) {
    Animal animal = new Dog();
    animal.move();
    }
    }

    在这个例子中,Animal类是父类,Dog类是子类,Dog类重写了父类的move()方法。在main()方法中,创建了一个Animal类的对象,但是该对象指向了一个Dog类的实例。调用animal.move()方法时,由于子类重写了move()方法,因此会调用子类的方法,输出结果为“狗可以跑和走”,实现了多态。

重载(overload)和重写(override)的区别?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

  • 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;

    方法重载的规则:

    1. 方法名一致,参数列表中参数的顺序,类型,个数不同。

    2. 重载与方法的返回值无关,存在于父类和子类,同类中。

    3. 可以抛出不同的异常,可以有不同修饰符。

  • 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。

访问修饰符publicprivateprotected、以及不写(默认)时的区别?

修饰符 同一个类中 同一个包中子类、无关类 不同包的子类 不同包的无关类
private
默认
protected
public

this关键字有什么作用?

this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。

this的用法在Java中大体可以分为3种:

1. 普通的直接引用,this相当于是指向当前对象本身

2. 形参与成员变量名字重名,用this来区分:

1
2
3
4
public Person(String name,int age){
this.name=name;
this.age=age;
}

3. 引用本类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
private String name;
private int age;

// 构造方法1
public Person() {
this("Unknown", 0); // 调用构造方法2
}

// 构造方法2
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

抽象类**(abstract class)和接口(interface)**有什么区别?

1. 接⼝的⽅法默认是 public ,所有⽅法在接⼝中不能有实现(Java 8 开始接⼝⽅法可以有默认实现),⽽抽象类可以有⾮抽象的⽅法。

2. 接⼝中除了 static 、 final 变量,不能有其他变量,⽽抽象类中则不⼀定。

3. ⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。接口自己本身可以通过extends 关键字扩展多个接⼝。

4. 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。

5. 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。

1. 在 JDK8 中,接⼝也可以定义静态⽅法,可以直接⽤接⼝名调⽤。实现类和实现是不可以调⽤的。如果同时实现两个接⼝,接⼝中定义了⼀样的默认方法,则必须重写,不然会报错。

2. jdk9 的接⼝被允许定义私有方法 。

总结⼀下 jdk7~jdk9 Java 中接⼝的变化:

1. 在 jdk 7 或更早版本中,接⼝⾥⾯只能有常量变量和抽象⽅法。这些接⼝⽅法必

须由选择实现接⼝的类实现。

2. jdk 8 的时候接⼝可以有默认⽅法和静态⽅法功能。

3. jdk 9 在接⼝中引⼊了私有⽅法和私有静态⽅法。

成员变量与局部变量的区别有哪些?

1. 从语法形式上看 :成员变量是属于类的,⽽局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被final 所修饰。

2. 从变量在内存中的存储⽅式来看 :如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。

3. 从变量在内存中的⽣存时间上看 :成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调⽤⽽⾃动消失。

4. 成员变量如果没有被赋初值 :则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。

静态变量和实例变量的区别?静态方法、实例方法呢?

静态变量和实例变量的区别?

  • 静态变量**:** 是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个副本。

  • 实例变量**:** 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。

静态方法和实例方法有何不同?

  • 静态方法:static修饰的方法,也被称为类方法。在外部调⽤静态⽅法时,可以使⽤”类名.方法名”的⽅式,也可以使⽤”对象名.方法名”的⽅式。静态方法里不能访问类的非静态成员变量和方法。

  • 实例方法:依存于类的实例,需要使用”对象名.”对象名.方法名”的⽅式调用;可以访问类的所有成员变量和方法。

final关键字有什么作用?

final表示不可变的意思,可用于修饰类、属性和方法:

  • 被final修饰的类不可以被继承

  • 被final修饰的方法不可以被重写

  • 被final修饰的变量不可变,被final修饰的变量必须被显式第指定初始值,还得注意的是,这里的不可变指的是变量的引用不可变,不是引用指向的内容的不可变。

finalfinallyfinalize的区别?

  • final 用于修饰变量、方法和类:final修饰的类不可被继承;修饰的方法不可被重写;修饰的变量不可变。

  • finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下, System.exit (0) 可以阻断 finally 执行。

  • finalize 是在 java.lang.Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在 gc 启动,该对象被回收的时候被调用。

    一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。

**==**和 equals 的区别?

  • == : 它的作⽤是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据类型**==比较的是值,引⽤数据类型==**比较的是内存地址)。

  • equals() : 它的作⽤也是判断两个对象是否相等。但是这个“相等”一般也分两种情况:

    • 默认情况:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“ == ”比较这两个对象,还是相当于比较内存地址。
    • 自定义情况:类覆盖了 equals() ⽅法。我们平时覆盖的 equals()方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Person {
    private String no;
    private String name;
    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Person)) return false;
    Person person = (Person) o;
    return Objects.equals(no, person.no) &&
    Objects.equals(name, person.name);
    }
    @Override
    public int hashCode() {
    return Objects.hash(no, name);
    }
    }

为什么重写 quals 时必须重写 hashCode ⽅法?

  • hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数,定义在 Object 类中, 是一个本地方法,这个⽅法通常⽤来将对象的内存地址转换为整数之后返回。

    哈希码主要在哈希表这类集合映射的时候用到,哈希表存储的是键值对(key-value),它的特点是:能根据“键”快速的映射到对应的“值”。这其中就利⽤到了哈希码!

  • hashCode()方法返回的哈希码用于判断两个对象是否相等,如果两个对象相等,它们的哈希码必须相等。因此,如果重写了equals()方法但未重写hashCode()方法,那么两个相等的对象可能会具有不同的哈希码,导致哈希表等数据结构无法正确地存储和访问这些对象。

Java是值传递,还是引用传递?

在 Java 中,所有的参数传递都是值传递,而不是引用传递。当你传递一个对象作为参数时,实际上是将对象的引用(内存地址)作为值传递给了方法。因此,方法中对对象的任何修改都会影响到原始对象。但是,如果在方法中将对象的引用指向一个新的对象,则原始对象不会受到影响。

深拷贝和浅拷贝?

  • 浅拷贝是指在复制对象时,只复制对象本身和其中的基本数据类型属性,而不复制其中的引用类型属性的对象。这意味着,新对象和原始对象中的引用类型属性指向的是同一个对象,对新对象中的引用类型属性进行修改会影响到原始对象中的引用类型属性。

  • 深拷贝是指在复制对象时,不仅复制对象本身和其中的基本数据类型属性,还会递归复制其中的引用类型属性的对象,以保证新对象和原始对象中的引用类型属性指向的是不同的对象。这样,对新对象中的引用类型属性进行修改不会影响到原始对象中的引用类型属性。

深拷贝如何实现呢?

  1. 通过实现 Serializable 接口并使用对象序列化/反序列化实现深拷贝:
1
2
3
4
5
6
7
8
9
10
public static <T> T deepClone(T obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
}
  1. 通过使用第三方库如 Apache Commons 的 BeanUtils 类中的 cloneBean 方法实现深拷贝:
1
2
3
public static <T> T deepClone(T obj) throws IllegalAccessException, InstantiationException, InvocationTargetException {
return (T) BeanUtils.cloneBean(obj);
}
  1. 通过实现 Cloneable 接口并重写 clone 方法实现深拷贝:
1
2
3
4
5
6
7
8
9
10
11
public class Person implements Cloneable {
private String name;
private List<String> hobbies;

@Override
public Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.hobbies = new ArrayList<>(this.hobbies);
return cloned;
}
}

Java 创建对象有哪几种方式?

  • new创建新对象

  • 通过反射机制

  • 采用clone机制

  • 通过序列化机制

前两者都需要显式地调用构造方法。对于clone机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在Java中序列化可以通过实现Externalizable或者Serializable来实现。

常用类

String的底层是什么

  • Java 8 及之前的版本:String 的底层是使用 char 数组来存储字符串的每个字符,每个字符占用 2 个字节(16 位)。

  • Java 9 开始,Java 使用了 Compact Strings 的技术来改变 String 的内部表示方式。

  • 当字符串中的字符都属于 Latin-1 字符集(即字符的 Unicode 值在 0 到 255 之间)时,String 的底层会使用 byte 数组来存储字符串的每个字符,每个字符只占用 1 个字节(8 位),这样可以节省内存空间。

  • 当字符串中的字符不属于 Latin-1 字符集时,String 的底层会使用一种称为 UTF-16 的编码方式来存储字符串的每个字符。

    UTF-16 是一种 Unicode 字符编码方式,它使用 16 位(2 个字节)来表示每个字符。对于字符的 Unicode 值在 0 到 65535 之间的范围内,UTF-16 使用一个 16 位的编码表示。而对于字符的 Unicode 值超过 65535 的范围,UTF-16 使用一种称为代理对(surrogate pair)的方式来表示。代理对是由两个 16 位的编码组成的,它们一起表示一个字符的 Unicode 值。其中,高代理项(high surrogate)的范围是 0xD800 到 0xDBFF,低代理项(low surrogate)的范围是 0xDC00 到 0xDFFF。通过组合高代理项和低代理项,可以表示超出基本多语言平面(BMP)的字符。

String和StringBuilderStringBuffer的区别?

  • String:String 的值被创建后不能修改,任何对 String 的修改都会引发新的 String对象的生成。

  • StringBuffer:跟 String 类似,但是值可以被修改,使用 synchronized 来保证线程安全。

  • StringBuilder:StringBuffer 的非线程安全版本,性能上更高一些。

String str1 = new String(“abc”)String str2 = “abc” 和 区别?

两个语句都会去字符串常量池中检查是否已经存在 “abc”,如果有则直接使用,如果没有则会在常量池中创建 “abc” 对象。

但是不同的是,String str1 = new String(“abc”) 还会通过 new String() 在堆里创建一个”abc” 字符串对象实例。所以后者可以理解为被前者包含。

String s = new String(“abc”)创建了几个对象?

很明显,一个或两个。如果字符串常量池已经有“abc”,则是一个;否则,两个。

当字符创常量池没有 “abc”,此时会创建如下两个对象:

  • 一个是字符串字面量 “abc” 所对应的、字符串常量池中的实例

  • 另一个是通过 new String() 创建并初始化的,内容与”abc”相同的实例,在堆中。

Object 类的常见方法有哪些?

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* naitive 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }

异常

Exception 和 Error 有什么区别?

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • ErrorError 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

Checked Exception 和 Unchecked Exception 有什么区别?

Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundExceptionSQLException…。

Unchecked Exception不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。

RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):

  • NullPointerException(空指针错误)
  • IllegalArgumentException(参数错误比如方法入参类型错误)
  • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
  • ArrayIndexOutOfBoundsException(数组越界错误)
  • ClassCastException(类型转换错误)
  • ArithmeticException(算术错误)
  • SecurityException (安全错误比如权限不够)
  • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
  • ……

I/O

JavaIO 流分为几种?

流按照不同的特点,有很多种划分方式。

  • 按照流的流向分,可以分为 输入流 和 输出流 ;

  • 按照操作单元划分,可以划分为 字节流 和 字符流 ;

  • 按照流的角色划分为 节点流 和 处理流

Java Io流共涉及40多个类,看上去杂乱,其实都存在一定的关联, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。

  • InputStream / Reader : 所有的输入流的基类,前者是字节输入流,后者是字符输入流。

  • OutputStream / Writer : 所有输出流的基类,前者是字节输出流,后者是字符输出流。

    image-20230721175136315

BIONIOAIO

image-20230721175417464

BIO(blocking I/O) : 就是传统的IO,同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过连接池机制改善(实现多个客户连接服务器)。

image-20230721175452573

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。

NIO :全称 java non-blocking IO,是指 JDK 提供的新 API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO)。

NIO是同步非阻塞的,服务器端用一个线程处理多个连接,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理:

image-20230724111208699

NIO的数据是面向缓冲区Buffer的,必须从Buffer中读取或写入。

所以完整的NIO示意图:

image-20230724111233872

可以看出,NIO的运行机制:

  • 每个Channel对应一个Buffer。

  • Selector对应一个线程,一个线程对应多个Channel。

  • Selector会根据不同的事件,在各个通道上切换。

  • Buffer是内存块,底层是数据。

AIO:JDK 7 引入了 Asynchronous I/O,是异步不阻塞的 IO。在进行 I/O 编程中,常用到两种模式:Reactor 和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理,完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

序列化

什么是序列化?什么是反序列化?

  • 序列化就是把Java对象转为二进制流,方便存储和传输。

  • 反序列化就是把二进制流恢复成对象。

类比我们生活中一些大件物品的运输,运输的时候把它拆了打包,用的时候再拆包组装。

Serializable接口有什么用?

这个接口只是一个标记,没有具体的作用,但是如果不实现这个接口,在有些序列化场景会报错,所以一般建议,创建的JavaBean类都实现 Serializable。

serialVersionUID 又有什么用?

serialVersionUID 就是起验证作用。

1
private static final long serialVersionUID = 1L;

我们经常会看到这样的代码,这个 ID 其实就是用来验证序列化的对象和反序列化对应的对象ID 是否一致。

这个 ID 的数字其实不重要,无论是 1L 还是 IDE自动生成的,只要序列化时候对象的 serialVersionUID 和反序列化时候对象的 serialVersionUID 一致的话就行。

如果没有显示指定 serialVersionUID ,则编译器会根据类的相关信息自动生成一个,可以认为是一个指纹。

所以如果你没有定义一个 serialVersionUID, 结果序列化一个对象之后,在反序列化之前把对象的类的结构改了,比如增加了一个成员变量,则此时的反序列化会失败。

因为类的结构变了,所以 serialVersionUID 就不一致。

Java 序列化不包含静态变量?

序列化的时候是不包含静态变量的。

如果有些变量不想序列化,怎么办?

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

transient 只能修饰变量,不能修饰类和方法。

说说有几种序列化方式?

  • Java对象流列化 :Java原生序列化方法即通过Java原生流(InputStream和OutputStream之间的转化)的方式进行转化,一般是对象输出流ObjectOutputStream 和对象输入流 ObjectI叩utStream 。

  • Json序列化:这个可能是我们最常用的序列化方式,Json序列化的选择很多,一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。

  • ProtoBuff序列化:ProtocolBuffer是一种轻便高效的结构化数据存储格式,ProtoBuff序列化对象可以很大程度上将其压缩,可以大大减少数据传输大小,提高系统性能。

泛型

Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

1
2
3
4
5
6
7
8
9
List<Integer> list = new ArrayList<>();
list.add(12);
//这里直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加,是可以的
add.invoke(list, "kl");
System.out.println(list);

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型

//在实例化泛型类时,必须指定T的具体类型
public class Generic<T> {
private T key;

public Generic(T key) {
this.key = key;
}

public T getKey() {
return key;
}
}

如何实例化泛型类:

1
Generic<Integer> genericInteger = new Generic<Integer>(123456);

泛型接口

1
2
3
4
5
6
class GeneratorImpl<T> implements Generator<T> {
@Override
public T method() {
return null;
}
}

实现泛型接口,指定类型:

1
2
3
4
5
6
class GeneratorImpl<T> implements Generator<String> {
@Override
public String method() {
return "hello";
}
}

泛型方法

1
2
3
4
5
6
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}

使用:

1
2
3
4
5
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );

泛型常用的通配符有哪些?

常用的通配符为: TEKV,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

什么是泛型擦除?

所谓的泛型擦除,官方名叫“类型擦除”。

Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的类型信息都会被擦掉。

也就是说,在运行的时候是没有泛型的。

例如这段代码,往一群猫里放条狗:

1
2
3
4
LinkedList<Cat> cats = new LinkedList<Cat>();
LinkedList list = cats; // 注意我在这里把范型去掉了,但是list和
cats是同一个链表!
list.add(new Dog()); // 完全没问题!

因为Java的范型只存在于源码里,编译的时候给你静态地检查一下范型类型是否正确,而到了运行时就不检查了。上面这段代码在JRE(Java运行环境)看来和下面这段没区别:

1
2
3
LinkedList cats = new LinkedList(); // 注意:没有范型!
LinkedList list = cats;
list.add(new Dog());

为什么要类型擦除呢?

主要是为了向下兼容,因为JDK5之前是没有泛型的,为了让JVM保持向下兼容,就出了类型擦除这个策略。

注解

说一下你对注解的理解?

Java注解本质上是一个标记,可以理解成生活中的一个人的一些小装扮,比如戴什么什么帽子,戴什么眼镜。

注解可以标记在类上、方法上、属性上等,标记自身也可以设置一些值,比如帽子颜色是绿色。

有了标记之后,我们就可以在编译或者运行阶段去识别这些标记,然后搞一些事情,这就是注解的用处。

例如我们常见的AOP,使用注解作为切点就是运行期注解的应用;比如lombok,就是注解在编译期的运行。

注解生命周期有三大类,分别是:

  • RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件

  • RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了

  • RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息

所以我上文写的是解析的时候,没写具体是解析啥,因为不同的生命周期的解析动作是不同的。

像常见的:

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

就是给编译器用的,编译器编译的时候检查没问题就over了,class文件里面不会有Override 这个标记。

再比如 Spring 常见的 Autowired ,就是 RUNTIME 的,所以在运行的时候可以通过反射得到注解的信息,还能拿到标记的值 required 。

1
2
3
4
5
6
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}

动态代理

动态代理是一种代理技术,它可以在运行时为一个对象创建一个代理对象。动态代理可以在不修改原始对象的情况下,在对象的方法调用前后加入额外的处理逻辑。

动态代理通常使用 java.lang.reflect 包中的 Proxy 类和 InvocationHandler 接口实现。我们需要定义一个 InvocationHandler 实现类,该实现类提供了代理对象调用方法时需要执行的代码。然后,我们可以使用 Proxy 类的静态方法创建代理对象,并将 InvocationHandler 实例传入该方法中。最终,我们得到的代理对象将作为原始对象的代表,当我们调用代理对象的方法时,InvocationHandler 实现类中的代码将被执行。

动态代理是一种非常有用的技术,它可以用于许多场景,例如拦截对象的方法调用,在方法调用前后执行额外的处理逻辑,实现面向切面编程(AOP)等。

反射

什么是反射?原理?

Java中的反射是指在运行时动态获取类的信息,并对类进行操作的机制。它可以让你在不知道类的信息的情况下使用类的方法、字段和构造函数。

使用反射的步骤如下:

  1. 获取类的Class对象:你可以通过使用Class.forName()方法来获取一个类的Class对象。

  2. 获取类中的信息:你可以通过使用Class对象的方法来获取类中的信息,例如方法、字段、构造函数等。

  3. 创建类的实例:你可以通过使用Class对象的newInstance()方法来创建一个类的实例。

  4. 调用类的方法:你可以通过使用Method对象的invoke()方法来调用类的方法。

反射最核心的四个类:

image-20230724113802796

反射的应用场景?

一般我们平时都是在在写业务代码,很少会接触到直接使用反射机制的场景。

但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。

像Spring里的很多 注解 ,它真正的功能实现就是利用反射。

就像为什么我们使用 Spring 的时候 ,一个 @Component 注解就声明了一个类为Spring Bean 呢?为什么通过一个 @Value 注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?

这些都是因为我们可以基于反射操作类,然后获取到类/属性/方法/方法的参数上的注解,注解这里就有两个作用,一是标记,我们对注解标记的类/属性/方法进行对应的处理;二是注解本身有一些信息,可以参与到处理的逻辑中。

新特性

JDK1.8都有哪些新特性?

  • 接口默认方法:Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字修饰即可

  • Lambda 表达式和函数式接口:Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中),使用 Lambda 表达式使代码更加简洁,但是也不要滥用,否则会有可读性等问题,《Effective Java》作者 Josh Bloch 建议使用 Lambda 表达式最好不要超过3行。

  • Stream API:用函数式编程方式在集合类上进行复杂操作的工具,配合Lambda表达式可以方便的对集合进行处理。

    Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。

    简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

  • 日期时间API:Java 8 引入了新的日期时间API改进了日期时间的管理。

  • Optional 类:用来解决空指针异常的问题。很久以前 Google Guava 项目引入了Optional 作为解决空指针异常的一种方式,不赞成代码被 null 检查的代码污染,期望程序员写整洁的代码。受Google Guava的鼓励,Optional 现在是Java 8库的一部分。

Lambda 表达式了解多少?

Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。

比如我们以前使用Runnable创建并运行线程:

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running before Java8!");
}
}).start();

这是通过内部类的方式来重写run方法,使用Lambda表达式,还可以更加简洁:

1
new Thread( () -> System.out.println("Thread is running since Java8!") ).start();

当然不是每个接口都可以缩写成 Lambda 表达式。只有那些函数式接口(Functional Interface)才能缩写成 Lambda 表示式。

所谓函数式接口(Functional Interface)就是只包含一个抽象方法的声明。针对该接口类型的所有 Lambda 表达式都会与这个抽象方法匹配。

Java8有哪些内置函数式接口?

JDK 1.8 API 包含了很多内置的函数式接口。其中就包括我们在老版本中经常见到的ComparatorRunnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。

除了这两个之外,还有Callable、Predicate、Function、Supplier、Consumer等等。

Optional了解吗?

Optional 是用于防范 NullPointerException 。

可以将 Optional 看做是包装对象(可能是 null , 也有可能非 null )的容器。当我们定义了 一个方法,这个方法返回的对象可能是空,也有可能非空的时候,我们就可以考虑用 Optional 来包装它,这也是在 Java 8 被推荐使用的做法。

1
2
3
4
5
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0)));// "b

Stream 流用过吗?

Stream 流,简单来说,使用 java.util.Stream 对一个包含一个或多个元素的集合做各种操作。这些操作可能是 中间操作 亦或是 终端操作。 终端操作会返回一个结果,而中间操作会返回一个 Stream 流。

image-20230724114739069

0%