万字干货|Java基础面试题(2022版)

作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」

前言

耗时3个月,忙里偷闲终于整理出这份面试手册,希望对大家有所帮助
关注公众号:【小牛呼噜噜】,即可获取面试手册后续更新动态 及 更多高质量原创文章
如果有不懂的,或者勘误,随时找我询问,公众号有我联系方式


概念常识

Java 语言有哪些特点?

  1. 简单易学

    Java 会让你的工作变得更加轻松,使你把关注点放在主要业务逻辑上
    尤其是Java语言没有指针,并提供了自动的垃圾回收机制,使得程序员不必为内存管理而担忧

  2. 面向对象

    具有代码扩展,代码复用等功能,其三大核心概念:封装,继承,多态
    详情见:https://mp.weixin.qq.com/s/Q1hABlF4kBhcyf3vnLrtFQ

  3. 支持多平台

    在一个平台上编写的任何应用程序都可以轻松移植到另一个平台上, 是Java 虚拟机实现平台无关性

  4. 安全性

    Java 被编译成字节码,由 Java 运行时环境解释。编译后会将所有的代码转换为字节码,人类无法读取。它使开发无病毒,无篡改的系统/应用成为可能

  5. 健壮性

    Java 有强大的内存管理功能,在编译和运行时检查代码,它有助于消除错误。

  6. 支持多线程

    多线程是指允许一个应用程序同时存在两个或两个以上的线程,用于支持事务并发和多任务处理。
    C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持
    Java除了内置的多线程技术之外,还定义了一些类、方法等来建立和管理用户定义的多线程。

  7. 动态性

    它具有适应不断变化的环境的能力,Java程序需要的类能够动态地被载入到运行环境,也可以通过网络来载入所需要的类。它能够支持动态内存分配,从而减少了内存浪费,提高了应用程序的性能。

  8. 分布式

    Java 提供的功能有助于创建分布式应用。使用远程方法调用(RMI),程序可以通过网络调用另一个程序的方法并获取输出。您可以通过从互联网上的任何计算机上调用方法来访问文件。这是革命性的一个特点,对于当今的互联网来说太重要了。

  9. 高性能

    Java 最黑的科技就是字节码编程,Java 代码编译成的字节码可以轻松转换为本地机器代码。
    如果解释器速度不慢,Java可以在运行时直接将目标代码翻译成机器指令,翻译目标代码的速度与C/C++的性能没什么区别。通过 JIT 即时编译器来实现高性能。

JVM、JRE和JDK的关系

  • JVM

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,JVM在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。因此Java语言可以实现跨平台。
字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
只要满足 JVM 规范,任何公司、组织或者个人都可以开发自己的专属 JVM。

  • JRE

JRE(Java Runtime Environment)是 Java 运行时环境。包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,但是系统缺省加载这个包,不能用于创建新程序。

  • JDK

Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等,它能够创建和编译程序。

什么是字节码?

字节码:Java源代码编译后产生的文件(即扩展名为.class的文件)。

.java源码是给人类读的,而.class字节码只面向JVM(Java虚拟机)

采用字节码的好处:

  • Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效的(但和无需GC的语言 c、c++、rust等的运行效率还是有所差距)
  • 由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

一般Java程序运行的流程:

Java源代码—->编译器—->jvm可执行的Java字节码(即虚拟指令)—->jvm—->jvm中解释器—–>机器可执行的二进制机器码—->程序运行

为什么说 Java 语言是“编译与解释并存”?

由于计算机只看得懂0100011机器码,语言像C语言,是给人看的。我们通过这些语言编写出来的代码,需要先转换成机器码,然后计算机才能去执行。

  • 编译型 :先将源代码一次性转换成另一种相对来说更低级的语言(字节码,汇编,机器码…),计算机再去执行。常见的编译性语言有 C、C++、Go、Rust 等等。

    注意这边的机器码 是代码对应的平台计算机的机器码

  • 解释型 :解释器动态将代码逐句解释(interpret)为机器码(编译器自身的机器码)并运行(边解释边执行)。常见的解释性语言有 Python、JavaScript、PHP 等等。


为什么经常说java是解释性语言也是编译型语言?

JVM的类加载器首先加载字节码文件,然后通过解释器逐行解释执行,每次执行都需要加载、解析,速度慢,还有热点代码重复加载问题。所以引进了JIT编译器(运行时编译),JIT完成一次编译后会将字节码对应的机器码保存下来,下次直接执行。

解释和编译都只是程序从源码到运行时的一种动作,跟语言本身无关,所以我们无需过于纠结这个问题!

Oracle JDK 和OpenJDK的区别

Java最早由SUN公司(Sun Microsystems,发起于美国斯坦福大学,SUN是Stanford University Network的缩写)发明,2006年SUN公司将Java开源,此时的JDK即为OpenJDK。
OpenJDK是Java SE的开源实现,他由SUN和Java社区提供支持,2009年Oracle收购了Sun公司,自此Java的维护方之一的SUN也变成了Oracle。
大多数JDK都是在OpenJDK的基础上编写实现的,比如IBM J9,Azul Zulu,Azul Zing和Oracle JDK。几乎现有的所有JDK都派生自OpenJDK,
他们之间不同的是许可证:

  • OpenJDK根据许可证GPL v2发布。
  • Oracle JDK根据Oracle二进制代码许可协议获得许可。

Oracle JDK 比 OpenJDK 更稳定。在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能
OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。

OpenJDK 是一个参考模型并且是完全开源的,
但是Oracle JDK是OpenJDK的一个实现,并不是完全开源的

Java 和 C++ 的区别?

Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是它们还是有挺多不相同:

  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
  • 等等

什么是JIT?

JIT是just in time的缩写,也就是即时编译。通过JIT技术,能够做到Java程序执行速度的加速。

Java通过编译器javac先将源程序编译成与平台无关的Java字节码文件(.class),再由JVM解释执行字节码文件,从而做到平台无关。 但是,有利必有弊。对字节码的解释执行过程实质为:JVM先将字节码翻译为对应的机器指令,然后执行机器指令。很显然,这样经过解释执行,其执行速度必然不如直接执行二进制字节码文件。

而为了提高执行速度,便引入了 JIT 技术。当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把部分“热点代码”编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用。

Java关键字

final finally finalize区别

final可以修饰类、变量、方法

  • 修饰类表示该类不能被继承
  • 修饰方法表示该方法不能被重写
  • 修饰变量表示该变量是一个常量,不可变,在编译阶段会存入常量池中

finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块
中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。

finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调
用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的
最后判断。

聊聊this、super和static关键字

  1. this关键字

    this表示当前对象的引用:
    this.属性 区别成员变量和局部变量
    this.() 调用本类的某个方法
    this() 表示调用本类构造方法,只能用在构造方法的第一行语句。
    this关键字只能出现在非static修饰的代码中

1
2
3
4
5
6
public class Member {
String name;
public void setName(String name) {
this.name = name;
}
}
  1. super关键字

    super可以理解为是指向自己超(父)类对象的一个”指针”,而这个超类指的是离自己最近的一个父类。:
    super.属性 表示父类对象中的成员变量
    super.方法()表示父类对象中定义的方法
    super() 表示调用父类构造方法
    可以指定参数,比如super(“Nanjin”);
    任何一个构造方法的第一行默认是super();
    可以写上,如果未写,会隐式调用super();
    super()只能在构造方法的第一行使用。
    this()和super()都只能在构造的第一行出现,所以只能选择其一。
    写了this()就不会隐式调用super()。
    super 关键字在子类中显式调用父类中被覆盖的非静态成员方法和成员变量

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
class Father {

void message() {
System.out.println("This is Father");
}

}

class Son extends Father {
void message() {
System.out.println("This is son");
}

void display() {
message();
super.message();
}

}

class Main {
public static void main(String args[]) {
Son s = new Son();
s.display();
}

}

结果:

This is son

This is father

  1. static关键字

    static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法

    static修饰的变量称之为静态变量
    被static修饰的变量或者方法是独立于该类的任何对象,即这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
    static修饰的方法称之为静态方法
    静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段
    static修饰的代码块叫做静态代码块。

被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。用法:“类.静态变量”

  1. 能否在static环境中访问非static变量?

     不能, static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员和静态方法,因为当static方法被调用时,这个类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法。同理,static方法也不能访问非static类型的变量。
    
  2. this与super关键字的区别

  • this 表示当前对象的引用,可以理解为指向对象本身的一个”指针”,但是JAVA中是没有指针这个概念的。
  • super 表示自己超(父)类对象的引用,可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
  • super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
  • this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。

相同点:

super()和this()均需放在构造方法内第一行。
this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。

拓展:https://mp.weixin.qq.com/s/tsbDfyYLqr3ctzwHirQ8UQ

Java 有没有 goto

goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。

基础语法

Java 中的基本数据类型有哪些?

Java 中有 8 种基本数据类型,分别为:

基本类型 位数 字节 默认值 包装类 取值范围
byte 8 1 0 Byte -128 ~ 127
short 16 2 0 Short -32768 ~ 32767
int 32 4 0 Integer -2147483648 ~ 2147483647
long 64 8 0L Long -9223372036854775808 ~ 9223372036854775807
char 16 2 ‘u0000’ Character 0 ~ 65535
float 32 4 0f Float 1.4E-45 ~ 3.4028235E38
double 64 8 0d Double 4.9E-324 ~ 1.7976931348623157E308
boolean 1
false Boolean true、false

其中:

char a = ‘hello’; 单引号
String a = “hello” ;双引号

包装类型的缓存机制

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能:
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,如果超出对应范围仍然会去创建新的对象
另外两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。

1
2
3
4
5
6
Integer i1 = 10;
Integer i2 = 10;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1 == i2);// 输出 true
System.out.println(i3 == i4);// 输出 false

什么是自动类型转换、强制类型转换?

Java 数值型变量经常需要进行相互转换,当把⼀个表数范围⼩的数值或变量直接赋给另⼀个表 数范围⼤的变量时,可以进⾏⾃动类型转换,即隐式转换

1
2
3
4
long l = 100;

int i = 200;
long ll = i;

反之,需要强制类型转换(显式转换)

1
2
3
4
5
short s = 199;
int i = s;// 199

double d = 10.24;
long ll = (long) d;// 10, 精度丢失

自动类型转换规则如下:

  • 数值型数据的转换:byte→short→int→long→float→double。
  • 字符型转换为整型:char→int。

一些常见的易错题:

  1. float f=3.4; 程序正确吗?

    3.4 是单精度数,将双精度型(double)赋值给浮点型(float)属于下转型会造成精度损失,因此需要强制类型转换 float f =(float)3.4; 或 者写成 float f =3.4F;

  2. 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);其中有隐含的强 制类型转换。

  3. int count = 100000000; int price = 1999; long totalPrice = count * price;正确吗?

    不正确,编译没任何问题,但结果却输出的是负数,这是因为两个 int 相乘得到的结果是 int, 相乘的结果超出了 int 的代表范围。这种情况,一般把第一个数据转换成范围大的数据类型再和其他的数据进行运算。


Math.round(11.5) 等于多少?Math.round(-11.5)等于多少

四舍五入的原理是在参数上加 0.5 然后进行下取整。

  1. Math.round(11.5)的返回值是 12
  2. Math.round(-11.5)的返回值是-11

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

2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。

运算符 | 和 ||,& 和 && 的区别

|& 定义为位运算符。
||&& 定义为逻辑运算符,

& 按位与操作,按二进制位进行”“运算。运算规则:(有 0 则为 0)
| 按位或运算符,按二进制位进行”“运算。运算规则:(有 1 则为 1)

1
2
3
4
5
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A | B = 0011 1101

而&&运算符是短路与运算。当且仅当两个为真,条件才真。如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要⽤&&⽽不是&。

**||**运算符是短路或运算,短路原理和&&同理

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

  • break 跳出整个循环,不再执⾏循环( 结束当前的循环体 )
  • continue 跳出本次循环,继续执⾏下次循环( 结束正在执⾏的循环 进⼊下⼀个循环条件 )
  • return 程序返回,不再执⾏下⾯的代码( 结束当前的⽅法 直接返回 )

    什么是自动拆箱/装箱?

    装箱 :将基本类型⽤它们对应的引⽤类型包装起来;
    拆箱 :将包装类型转换为基本数据类型;
    Java可以⾃动对基本数据类型和它们的包装类进⾏装箱和拆箱。
    我们来看一下基本类型跟封装类型之间的对应关系:
数据类型 封装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

我们来看一个例子:

1
2
3
4
// 自动装箱
1. Integer a = 100;
// 自动拆箱
2. int b = a;

自动装箱,相当于Java编译器替我们执行了 Integer.valueOf(XXX);
自动拆箱,相当于Java编译器替我们执行了Integer.intValue(XXX);

Integer a= 127 与 Integer b = 127相等吗

对于对象引用类型:==比较的是对象的内存地址。
对于基本数据类型:==比较的是值。
如果整型字面量的值在**-128到127**之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
System.out.println(b == c); // true

Integer a1 = 128;
Integer b1 = 128;
System.out.println(a1 == b1); // false

Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true
}

自增自减运算

++和–运算符可以放在变量之前,也可以放在变量之后。
当运算符放在变量之前时(前缀),先⾃增/减,再赋值;当运算符放在变量之后时(后缀),先赋 值,再⾃增/减。

1
2
3
int i = 1;
i = i++;
System.out.println(i);

结果为1
如果将i++ 换成++i, 则结果为:2

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

Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java 中引⼊了枚举类型, expr 也可以是 enum 类型。
从 Java 7 开始,expr还可以是字符串(String),但是长整型(long)在⽬前所有的版本中都是不可 以的。

静态变量和实例变量区别

静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。

实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。

静态变量与普通变量区别

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。


面向对象

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

  1. 面向过程:
  • ⾯向过程就是分析出解决问题所需要的步骤,然后⽤函数把这些步骤⼀步⼀ 步实现,使⽤的时候再⼀个⼀个的⼀次调⽤就可以
  • 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
  • 缺点:没有面向对象易维护、易复用、易扩展
  1. 面向对象:
  • ⾯向对象,把构成问题的事务分解成各个对象,⽽建⽴对象的⽬的也不是为 了完成⼀个个步骤,⽽是为了描述某个事件在解决整个问题的过程所发⽣的⾏为。 ⽬的是 为了写出通⽤的代码,加强代码的重⽤,屏蔽差异性
  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
  • 缺点:性能比面向过程低


面向对象的3大特性是什么?

  1. 封装

封装把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。换句话说就是 把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。

  1. 继承

**继承 **就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

在 Java 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:

1
2
3
4
5
class 父类 {
}

class 子类 extends 父类 {
}


继承概念的实现方式有二类:实现继承接口继承

  • 实现继承是指直接使用基类的属性和方法而无需额外编码的能力
  • 接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力
  • 一般我们继承基本类和抽象类用 extends 关键字,实现接口类的继承用 implements 关键字。

注意点:

  • 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
  • 继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。
  • ⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属 性和⽅法⼦类是⽆法访问,只是拥有
  • 子类可以拥有自己的属性和方法, 即⼦类可以对⽗类进⾏扩展。
  • 子类可以重写覆盖父类的方法。
  • JAVA 只支持单继承,即一个子类只允许有一个父类,但是可以实现多级继承,及子类拥有唯一的父类,而父类还可以再继承。

使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

1
2
3
4
5
6
7
8
9
10
11
12
13
# implements 关键字

public interface A {
public void eat();
public void sleep();
}

public interface B {
public void show();
}

public class C implements A,B {
}
  1. 多态

同一个行为具有多个不同表现形式或形态的能力就是 多态。网上的争论很多,笔者个人认同网上的这个观点:重载也是多态的一种表现,不过多态主要指运行时多态

Java 多态可以分为 重载式多态重写式多态

  • 重载式多态,也叫编译时多态。编译时多态是静态的,主要是指方法的重载overload,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。也就是说这种多态再编译时已经确定好了。
  • 重写式多态,也叫运行时多态。运行时多态是动态的,主要指继承父类和实现接口override时,可使用父类引用指向子类对象实现这个就是大家通常所说的多态性

这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。 这种多态可通过函数的重写以及向上转型来实现。
多态存在的三个必要条件(实现方式):

  • 继承
  • 重写
  • 父类引用指向子类对象:Parent p = new Child();

关于继承如下 3 点请记住:

  • 子类拥有父类非 private 的属性和方法。
  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。

拓展: https://mp.weixin.qq.com/s/Q1hABlF4kBhcyf3vnLrtFQ

抽象类和接口的区别是什么?


相同点:

  • 都不能被实例化
  • 都包含抽象方法,其子类都必须覆写这些抽象方法
  • 都位于继承的顶端,用于被其他实现或继承

不同点在于:

  1. 接口, 通常以interface来声明

    1
    2
    3
    public interface UserIF() {
    //定义
    }
  2. 抽象类, 通常以abstract来声明

    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
    public abstract class Employee {
    private String name;
    private String address;

    public String getName() {
    return name;
    }
    public void setName(String name) {
    this.name = name;
    }
    public String getAddress() {
    return address;
    }
    public void setAddress(String address) {
    this.address = address;
    }
    // 无参构造
    public Employee() {
    }
    // 有参构造
    public Employee(String name, String address) {
    this.name = name;
    this.address = address;
    }
    // 抽象的方法
    public abstract Integer num(Integer a, Integer b);

    }
  3. 抽象类中可以进行方法的定义和实现; 在接口中,只允许进行方法的定义,不允许有方法的实现
    由于Java 8 可以用 default 关键字在接口中定义默认方法,所以2者都可以有默认实现的方法

  4. 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值

  5. 变量:接口中定义的变量只能是公共的静态常量,抽象类中的变量是普通变量。

  6. Java中只能extends继承一个类,但是可以implements继承多个接口

  7. 抽象级别不同:抽象程度由高到低依次是 接口 > 抽象类 > 类

  8. 类如果要实现一个接口,它必须要实现接口声明的所有方法。但是,类可以不实现
    抽象类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。

  9. Java 接口中声明的变量默认都是 static 和 final 的。抽象类可以包含非 final 的变量

补充:抽象类能使用 final 修饰吗?

不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类

普通类和抽象类有哪些区别?

普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。

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


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

重载发⽣在⼀个类中,同名的⽅法如果有不同的参数列表(参数类型不同、参数个数不同
或者⼆者都不同)则视为重载;

重写发⽣在⼦类与⽗类之间,重写要求⼦类被重写⽅法与⽗类被重写⽅法有相同的返回类
型,⽐⽗类被重写⽅法更好访问,不能⽐⽗类被重写⽅法声明更多的异常(⾥⽒代换原
则)。

⽅法重载的注意事项:

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

  2. 重载与⽅法的返回值⽆关,存在于⽗类和⼦类,同类中。

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

    面向对象五大基本原则是什么

  4. 单一职责原则SRP(Single Responsibility Principle)

类的功能要单一,不能包罗万象,跟杂货铺似的。

  1. 开放封闭原则OCP(Open-Close Principle)

一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。

  1. 里式替换原则LSP(the Liskov Substitution Principle LSP)

子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~

  1. 依赖倒置原则DIP(the Dependency Inversion Principle DIP)

高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。

  1. 接口分离原则ISP(the Interface Segregation Principle ISP)

设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。

== 和 equals 的区别是什么

  1. 关于 ==

== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。

对于基本类型和引用类型 == 的作用效果是不同的:

  • 基本类型:比如 byte, short, char, int, long, float, double, boolean, 他们比较的是他们的值;
  • 引用类型:比较的是他们在内存中的存放地址,更准确的说,是堆内存地址

如果是具体的阿拉伯数字的比较,值相等则为true,如:
int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。

  1. 关于 equals

equals一般意义上是比较的是两个对象的内容是否相等,如String类中equals。但是由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行重写的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。

String s="abcd"是一种非常特殊的形式,和new 有本质的区别。
它是java中唯一不需要new 就可以产生对象的途径。以String s="abcd";形式赋值在java中叫直接量,它是在常量池中而不是象new一样放在压缩堆中。这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为"abcd"的对象,如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个”abcd”,下一次如果有String s1 = "abcd";又会将s1指向”abcd”这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象.

String s = new String("abcd");和其它任何对象一样.每调用一次就产生一个对象,只要它们调用。
补充:任何非空的引用值X,x.equals(null)的返回值一定为false 。
来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class test1 {
public static void main(String[] args) {
String a = new String("abcd"); // a 为一个引用
String b = new String("abcd"); // b为另一个引用,对象的内容一样
String aa = "abcd"; // 放在常量池中
String bb = "abcd"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
}
}

详情见:https://mp.weixin.qq.com/s/zbhftxnvnoTFY-RSegTjRQ

为什么重写 equals 时必须重写 hashCode ⽅法

  1. 什么是HashCode?

hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数,定义在 Object 类中,这个⽅法通常⽤来将对象的内存地址转换为整数之后返回。这也意味着Java中的任何类都包含有hashCode()函数。

  1. 为什么要有 hashCode ?

哈希码主要在哈希表这类集合映射的时候⽤到,哈希表存储的是键值对(key-value),它的特点 是:能根据“键”快速的映射到对应的“值”。 比如HashMap怎么把key映射到对应的value上呢?⽤的就是哈希取余法,也就是拿哈希码和存 储元素的数组的长度取余,获取key对应的value所在的下标位置

  1. 为什么重写 quals 时必须重写 hashCode ⽅法?
  • 如果两个对象相等,则hashcode一定也是相同的
  • 两个对象相等,对两个对象分别调用equals方法都返回true
  • 两个对象有相同的hashcode值,它们也不一定是相等的(哈希碰撞)

hashCode() 的默认⾏为是对堆上的对象产⽣独特值,如果没有重写 hashCode() ,则该class 的 两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)。因 此,equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖。

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

传递的过程的参数一般有2种情况值传递和引用传递。

  • 值传递:调用函数时将实际参数复制一份传递到函数中,函数内部对参数内部进行修改不会影响到实际参数,即创建副本,不会影响原生对象
  • 引用传递 :方法接收的是实际参数所引用的地址,不会创建副本,对形参的修改将影响到实参,即不创建副本,会影响原生对象

在Java中有2种数据类型,其中主要有基本数据类型引用数据类型,除了8中基本数据类型以外都是引用数据类型,8中基本数据类型分别是byte,short,int,long,char,boolean,float,double

** Java只有值传递**,参数如果是基本数据类型,复制的是具体值;如果参数是引用类型,把地址当成值,复制的是地址;还有String类是一个非常特殊的类,她是不可变的。

详情可见:https://mp.weixin.qq.com/s/6qRspyLAsoBxttGwGtxsAA

深拷贝、浅拷贝、引用拷贝?

  1. 引用拷贝:引用拷贝会在栈上生成一个新的对象引用地址,但是两个最终指向依然是堆中同一个对象
  2. 浅拷贝:浅拷贝会在堆上创建一个新对象,新对象和原对象本身没有任何关系,新对象和原对象不等,但是新对象的属性和老对象相同

具体可以看如下区别:

  • 如果属性是基本类型(int,double,long,boolean等),拷贝的就是基本类型的值;
  • 如果属性是引用类型,拷贝的就是内存地址(即复制引用但不复制引用的对象),也就是说拷贝对象和原对象共用同一个内部对象。因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
  1. 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象,堆中的对象也会拷贝⼀ 份

浅拷贝如何实现呢?

Object类提供的clone()⽅法可以⾮常简单地实现对象的浅拷贝。

深拷贝如何实现呢?

  1. 重写克隆⽅法:重写克隆⽅法,引⽤类型变量单独克隆,这⾥可能会涉及多层递归。
  2. 序列化:可以先讲原对象序列化,再反序列化成拷贝对象

Java 创建对象有哪⼏种⽅式

创建对象方式 是否调用了构造器
new关键字
Class.newInstance
Constructor.newInstance
Clone
反序列化

String相关

字符型常量和字符串常量的区别

  • 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
  • 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
  • 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)

    String 是最基本的数据类型吗

    不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype)
    由于String 类使⽤ final 修饰,不可变,无法被继承

什么是字符串常量池?

字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。

String、StringBuffer、StringBuilder 的区别

String:String 的值被创建后不能修改,任何对 String 的修改都会引发新的 String 对象的⽣ 成,: 适用操作少量的数据
StringBuilder:StringBuffer 的⾮线程安全版本,性能上更⾼⼀些 ,适合单线程操作字符串缓冲区下操作大量数据
StringBuffer:跟 String 类似,但是值可以被修改,使⽤ synchronized 来保证线程安全。适合多线程操作字符串缓冲区下操作大量数据

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

⼀个或两个
如果字符串常量池已经有“abc”,则是⼀个;
否则,两个。 当字符创常量池没有 “abc”,此时会创建如下两个对象: ⼀个是字符串字⾯量 “abc” 所对应的、字符串常量池中的实例 另⼀个是通过 new String() 创建并初始化的,内容与”abc”相同的实例,在堆中。

String str=”abc”与 String str=new String(“abc”)一样吗?

不一样,因为内存的分配方式不一样。String str="abc"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String("abc") 则会被分到堆内存中。

String有哪些特性

  • 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
  • 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。

其中 不可变是由于:

  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

String 类的常用方法都有那些?

  • indexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。
  • equals():字符串比较。

在使用 HashMap 的时候,用 String 做 key 有什么好处?

HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

异常

Exception 和 Error 有什么区别?

在 Java 中, Throwable 是 所有错误或异常的基类。Throwable 又分为 Error 和 Exception :

  1. Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)
  • CheckedException受检异常:编译器会强制检查并要求处理的异常。
  • RuntimeException运⾏时异常:程序运⾏中出现异常,⽐如我们熟悉的空指针、数组下标 越界等等
  1. Error :Error 属于程序无法处理的错误 ,不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)

try-catch-finally 如何使用?

  • try块 : 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块 : 用于处理 try 捕获到的异常。
  • finally 块 : 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

我们来看看几个常见的题目:

  1. 题目(1)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class TestReflection2 {
    public static void main(String[] args) {
    System.out.println(test());
    }
    public static int test() {
    try {
    return 1;
    } catch (Exception e) {
    return 2;
    } finally {
    System.out.print("3");
    }
    }
    }

    结果:31,在 return 前会先执⾏ finally 语句块,所以是先输出 finally ⾥ 的 3,再输出 return 的 1。

  2. 题目(2)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static void main(String[] args) {
    System.out.println(test());
    }

    public static int test() {
    try {
    return 1;
    } catch (Exception e) {
    return 2;
    } finally {
    return 3;
    }

    结果: 3。 try在return返回前先执⾏ finally,结果 finally 里不按套路出牌,直接 return 了,⾃然也就⾛不到 try ⾥⾯的 return 了 ,注意实际开发中不能在finally中直接return

  3. 题目(3)

    1
    2
    3
    4
    5
    6
    7
    8
    try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
    } catch (Exception e) {
    System.out.println("Catch Exception" );
    } finally {
    System.out.println("Finally");
    }

    结果:

    1
    2
    3
    Try to do something
    Catch Exception
    Finally

    没有return的话,try -catch-finally 依次执行

  4. 题目(4)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class TestReflection2 {
    public static void main(String[] args) {
    System.out.println(test1());
    }
    public static int test1() {
    int i = 0;
    try {
    i = 2;
    return i;
    } finally {
    i = 3;
    }
    }

    }

    结果:2, 在执⾏ finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执⾏完毕后,会返 回之前暂存的结果,⽽不是返回 i,所以即使 i 已经被修改为 3,最终返回的还是之前暂存起 来的结果 2

  5. 题目(5)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
    } catch (Exception e) {
    System.out.println("Catch Exception" );
    // 终止当前正在运行的Java虚拟机
    System.exit(1);
    } finally {
    System.out.println("Finally");
    }

    结果:

    1
    2
    ry to do something
    Catch Exception

    我们可以发现,finally 中的代码一般是一定会执行的,除了 2 种特殊情况下,finally 块的代码也不会被执行:程序所在的线程死亡、关闭 CPU

    IO

    Java 中 IO 流分为几种?

  • 按照流的流向分,可以分为输入流和输出流;
  • 按照操作单元划分,可以划分为字节流和字符流;
  • 按照流的角色划分为节点流和处理流。

BIO,NIO,AIO 有什么区别?

BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。

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

BIO⽅式适⽤于连接数⽬⽐较⼩且固定的架构,这种⽅式对服务器资源要求⽐较⾼,并发局限 于应⽤中,JDK1.4 以前的唯⼀选择,程序简单易理解

NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。

NIO的数据是⾯向缓冲区Buffer的,必须从Buffer中读取或写⼊ ,支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发

AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

当有事件触发时,服务器端得到通 知,进⾏相应的处理,完成后才通知服务端程序启动线程去处理,⼀般适⽤于连接数较多且 连接时间较长的应⽤


既然有了字节流,为什么还要有字符流?

其实字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还⽐较耗时,并且,如 果我们不知道编码类型就很容易出现乱码问题。
所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼜,⽅便我们平时对字符进⾏流操作。如 果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐较好

扩展:重要知识点

什么是反射?

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

常见的场景:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性

反射的原理? 我们都知道Java程序的执⾏分为编译和运⾏两步,编译之后会⽣成字节码(.class)⽂件,JVM进 ⾏类加载的时候,会加载字节码⽂件,将类型相关的所有信息加载进⽅法区,反射就是去获 取这些信息,然后进⾏各种操作。

拓展: https://mp.weixin.qq.com/s/_n8HTIjkw7Emcunpb4-Iwg

聊聊你认识的注解?

注解(Annotation ), 是 Java5 开始引入的新特性,是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,是一种标记、标签。注释往往会被编译器直接忽略,能够被编译器打包进入class文件,并执行相应的处理。

详情:https://mp.weixin.qq.com/s/2tmeI_rFY7mn6xdDs9eMxg

动态代理的原理

动态代理无需声明式的创建java代理类,而是在运行过程中动态生成"代理类",即编译完成后**没有实际的class文件**,而是在**运行时动态生成 类字节码**,并加载到JVM中。从而避免了静态代理那样需要声明大量的代理类。

详情:https://mp.weixin.qq.com/s/1nDO2gQxwjBGPYP-694fSA


感谢看到结尾,更多精彩文章尽在公众号:【小牛呼噜噜】,谢谢