本文主要记录面试中较高频率被问到的一些Java基础知识点
一、Java中为什么只有按值传递?
按值传递: 表示方法接收的是调用者提供的值
引用调用: 表示方法接收的是调用者提供的变量地址
一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言中方法参数传递方式。
Java 程序设计语言总是采用按值调用。方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
下面举例子进行说明:
(一)基本数据类型:
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
// 输出结果:
// a = 20
// b = 10
// num1 = 10
// num2 = 20
在 swap 方法中,a、b 的值进行交换,但是并不会影响到 num1、num2。原因是a、b 中的值是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。
通过上面例子,可以知道一个方法不能修改一个基本数据类型的参数
(二)引用数据类型:
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
// 输出结果:
// 1
// 0
array 被初始化 arr 的拷贝也就是一个对象的引用,所以 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。
通过上面例子可知,实现一个改变对象参数状态的方法并不是一件难事。
理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
总结:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
二、== 和 equals 的区别?
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。基本数据类型的 == 比较的是值,引用数据类型 == 比较的是内存地址
equals : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 1、类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 2、类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true。
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
三、为什么重写 equals 时必须重写 hashCode 方法?
(一)hashCode() 的作用
hashCode() 的作用是获取哈希码。它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
(二)hashCode()与 equals()的相关规定
- 如果两个对象相等,则 hashcode 一定也是相同的
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true
- 两个对象有相同的 hashcode 值,它们也不一定是相等的
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等
四、构造器(Constructor)是否可被重写?
有时候我们会看到一个类中有多个构造函数,所以构造器是不能被重写的(override),但是构造器可以重载 (overload)。
重载:同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理
重写:子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
五、重载与重写的区别?
重载发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同;重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
区别点 | 重载 | 重写 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 不能修改 |
参数列表 | 必须修改 | 不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
重写注意点:
- 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为 private / final / static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明
- 构造方法无法被重写
六、什么是深拷贝、浅拷贝?
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。
七、字符型常量和字符串常量的区别?
- 形式上:字符常量是单引号引起的一个字符;字符串常量是双引号引起的若干个字符
- 含义上:字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)
- 内存大小:字符常量只占 2 个字节;字符串常量占若干个字节
八、Java 面向对象编程三大特性?
(一)封装
把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。
(二)继承
使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
(三)多态
指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在 Java 中有两种形式可以实现多态:继承和接口
九、自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
装箱过程是通过调用包装器的valueOf方法实现的
而拆箱过程是通过调用包装器的 xxxValue方法实现的(xxx代表对应的基本数据类型)。
十、接口和抽象类的区别?
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。
- 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
- 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
备注:
在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。
JDK9 的接口被允许定义私有方法 。
JDK7~JDK9 Java 中接口概念的变化:
- 在 jdk 7 以前,接口里面只能有常量变量和抽象方法,接口方法必须由选择实现接口的类实现。
- jdk 8 的时候接口可以有默认方法和静态方法功能。
- Jdk 9 在接口中引入了私有方法和私有静态方法
十一、成员变量与局部变量的区别?
- 语法形式:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 变量在内存中的存储方式:如果成员变量是使用static修饰的,则属于类;如果没有使用static修饰,则属于实例。对象存于堆内存,如果局部变量类型为基本数据类型,存储在栈内存;如果为引用数据类型,存放的是指向堆内存对象的引用或者是指向常量池中的地址。
- 变量在内存中的生存时间:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
- 赋值方面:成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
十二、一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?
类的构造方法主要作用是完成对类对象的初始化工作。若一个类没有声明构造方法,该程序可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
十三:构造方法有哪些特性?
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
十四、静态方法和实例方法有何不同
-
在外部调用静态方法时,可以使用“类名.方法名”的方式,也可以使用“对象名.方法名”的方式,无需创建对象。而实例方法只有“对象名.方法名”这种方式。
-
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
十五:对象的相等与指向他们的引用相等,两者有什么不同?
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
十六、在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
帮助子类做初始化工作。
十七、在 Java 中定义一个不做事且没有参数的构造方法的作用
Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
十八、 final 关键字作用?
final 关键字主要用在三个地方:变量、方法、类。
- 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
- 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
- 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。
十九、Java 序列化中如果有些字段不想进行序列化,怎么办?
使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
二十、 StringBuffer 和 StringBuilder 的区别? String 为什么不可变?
(一)String 为什么是不可变的?
String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串 private final byte[] value
(二) String StringBuffer 和 StringBuilder 的区别?
线程安全性: StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
性能: 类型进行改变的时候,StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
三者使用的总结:
操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
本文参考来源:
JavaGuide面试突击版