[八股速成|秋招冲击SSP】JAVA基础(二)
1. 静态⽅法和实例⽅法有何不同
在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
为什么在一个静态方法内调用一个非静态成员为什么是非法的?
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
究其原因是因为类加载机制:有static关键字修饰,会先加载在内存中,且只执行一次。
2. 对象的相等与指向他们的引⽤相等,两者有什么不同?
1.引用相等
比较两个变量是否指向同一内存地址,使用 ==
运算符。
String s1 = "hello"; String s2 = s1; System.out.println(s1 == s2); // 输出 true:ml-citation{ref="7" data="citationList"}
2.对象相等
比较两个对象在逻辑或内容上是否一致,需通过 equals()
方法实现。
String s3 = new String("hello"); String s4 = new String("hello"); System.out.println(s3.equals(s4)); // 输出 true:ml-citation{ref="7" data="citationList"}
维度 |
引用相等 |
对象相等 |
比较方式 |
|
重写 |
默认行为 |
直接比较内存地址 |
未重写时等价于 |
关联方法 |
无 |
需同时重写 |
3. 在调⽤⼦类构造⽅法之前会先调⽤⽗类没有参数的构造⽅法,其⽬的是?
一.核心目的
1.保障对象初始化完整性
子类对象包含父类成员的实例变量,需先初始化父类成员后才能正确构建子类自身的逻辑15。例如:若父类属性未初始化,子类继承的 parentValue
可能为默认值或无效状态:
class Parent { int parentValue; } //未初始化则默认为0 class Child extends Parent { Child() { System.out.println(parentValue); } //可能输出0而非预期值 }
2.强制层级初始化顺序
Java通过构造方法调用链实现自顶向下的初始化流程,确保父类构造逻辑先于子类执行
4. 为什么重写equals()时一定要重写hashCode()?
1. java官方文档规定重写equals()时一定要重写hashCode()。 equals()和 hashCode()是 Object 类中的两个基础方法,Object 中的 equals( )和hashCode( ),使用==判断,即比较两个对象的地址值。
2. 但是在实际的开发中我们需要比较两个字符串对象的内容是否相等,所以需要重写euqals()。重写后的equals()先比较对象的地址值,再比较字符串对象的内容(通过循环遍历每个字符)
3. 哈希表比较两个对象是否相等,是先比较两个对象的hashcode值,所以需要重写hashcode(),(hashcode()不重写比较的是对象的引用地址,不是哈希值 ),使用hashcode()比较结果是fasle 则equals()不会再执行,若hashcode()返回true,再用equals()比较,之所以这样设计就是为了提高效率并解决哈希冲突。
以上可以总结为:
两个对象使用equals比较相同,hashCode一定相同
hashCode相同,equals()比较不一定相同,可能出现hash冲突
hashCode不同,equals()一定不同
拓展:
底层基于哈希表的数据结构和类主要有以下几种:
HashMap |
最常用的键值对存储容器 |
数组 + 链表(或红黑树) |
非线程安全,性能高,允许键为null,值为null |
HashSet |
基于 HashMap 实现的无序集合,元素不能重复 |
依赖 HashMap(元素作为键,虚拟对象作为值) |
不保证元素的插入顺序,无序 |
LinkedHashMap |
继承自 HashMap,保留插入顺序 |
HashMap + 双向链表 |
迭代时按插入顺序排序,略低于 HashMap 性能 |
LinkedHashSet |
继承自 HashSet,保留元素的插入顺序 |
LinkedHashMap 作为底层实现 |
保留插入顺序,元素不重复 |
Hashtable |
类似于 HashMap 的线程安全版本,但效率较低 |
数组 + 链表(或红黑树) |
线程安全,不允许null键或null值,逐渐被淘汰 |
ConcurrentHashMap |
支持高并发的线程安全哈希表实现 |
分段锁(Java 8之后采用同步机制优化) |
高并发性能,支持多线程操作,无锁或细粒度锁机制 |
5. 为什么Java中只有值传递?
在 Java 中,所有的参数传递都是通过 值传递(pass-by-value)进行的。这是 Java 中函数调用的一种基本规则。
1. 值传递的定义
值传递意味着 函数接收到的是参数值的副本。当你将一个变量传递给方法时,方法接收到的是该变量的值的副本,而不是原始变量本身。因此,无论在方法中如何修改这个副本,原始变量的值都不会受到影响。
2. Java 中的值传递
无论传递的是基本数据类型(如 int, char, float 等)还是对象类型(如 String, List, 自定义类对象等),Java 都是通过值传递的。这里的“值传递”是指:
- 基本数据类型:传递的是变量的值,即数据本身的副本。修改副本不会影响原始变量。
- 对象类型:传递的是对象引用的值,也就是说,传递的是对象在内存中的地址(即引用)的副本。这个副本指向同一个对象,但你不能通过副本来修改原始的引用。你仍然可以通过这个副本修改对象的内部状态。
3. 基本数据类型的值传递
对于基本数据类型,Java 直接传递的是数据的副本。因此,在方法内部修改该副本,不会影响外部变量。
public class Main { public static void main(String[] args) { int a = 10; modify(a); System.out.println(a); // 输出:10 } public static void modify(int num) { num = 20; // 修改副本,不会影响原始变量 } }
在上面的例子中,a 的值在 modify 方法中被修改为 20,但是原始的 a 变量保持不变。原因是 num 是 a 的副本,修改 num 不会影响到 a。
4. 对象的值传递
对于对象,传递的是对象引用的副本。这意味着传递的其实是对象地址的副本,即指向同一个对象。通过这个副本可以修改对象的内部状态,但是修改引用本身(让引用指向其他对象)不会影响原始的引用。
class Person { String name; Person(String name) { this.name = name; } } public class Main { public static void main(String[] args) { Person p = new Person("John"); modify(p); System.out.println(p.name); // 输出:Jane } public static void modify(Person person) { person.name = "Jane"; // 修改对象的内部状态 } }
5. Java 中为什么只有值传递
Java 只有值传递的原因主要有以下几点:
- 简化语言设计:如果 Java 允许引用传递(pass-by-reference),将会增加很多复杂性。例如,如果传递引用会影响原始对象,开发者可能会面临很难预测的副作用和难以调试的错误。
- 统一的内存管理:通过值传递,Java 对象的引用和基本数据类型的传递规则统一,简化了语言的内存管理。开发者不需要担心引用传递会改变对象本身,或者导致多次修改的问题。
- 避免指针的复杂性:一些语言(如 C++)允许通过指针传递引用,这带来了内存管理和指针操作上的复杂性。Java 不允许指针操作,采用值传递(尤其是引用传递的副本)可以避免这些复杂性。
6. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
1. 程序(Program):
- 定义:程序是指一段存储在磁盘等存储介质上的静态代码,包括可执行文件、脚本等,它们在未运行时只是普通的文件,不占用 CPU 资源。程序描述了一系列指令,告诉计算机应该做什么。
- 特点:程序是静态的,它只是一段代码,只有在执行时才会变成动态的进程。
2. 进程(Process):
- 定义:进程是一个正在运行的程序实例。当程序被操作系统执行时,它就变成了一个进程,进程是程序的动态表现形式。
- 特点:进程有自己独立的内存空间和系统资源,比如文件句柄、网络连接等。操作系统通过进程来分配资源,进程是系统资源管理的最小单位。每个进程可以包含多个线程。
- 生命周期:进程从创建、执行到终止的整个过程叫做进程的生命周期。操作系统会为每个进程分配一定的资源(如内存、CPU 时间片等)。
3. 线程(Thread):
- 定义:线程是进程内部的执行单元。一个进程可以包含多个线程,每个线程可以独立执行任务,并共享进程的内存和其他资源。
- 特点:线程是操作系统调度的基本单位,一个进程中的多个线程可以并发执行。线程之间共享进程的内存空间,但每个线程有自己的栈(栈用于存储局部变量和函数调用)。多个线程可以同时执行,提高程序的并发性。
4. 它们之间的关系:
- 程序到进程:程序是静态的代码文件,而进程是程序的运行实例。当你运行一个程序时,操作系统会为它创建一个进程,这时程序才开始占用 CPU 和内存资源。
- 进程和线程的关系:进程是资源分配的单位,而线程是 CPU 调度的单位。每个进程至少有一个线程(即主线程),但可以创建多个线程来执行不同的任务。这意味着:一个进程可以有多个线程,这些线程之间共享进程的资源(如内存、文件等)。线程的执行是独立的,但它们共享进程的资源,因此在多线程编程中需要注意线程安全问题。
总结:
- 程序是静态的指令集合,存储在磁盘上。
- 进程是程序的动态运行实例,它占用系统资源,是操作系统进行资源分配的基本单位。
- 线程是进程内部的执行单元,是操作系统调度的基本单位,线程可以并发执行,提高程序的并发性。
7. 线程有哪些基本状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》)。
.
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
8. 关于final关键字的⼀些总结
final关键字的作⽤是什么?
- 修饰类:表示类不可被继承
- 修饰⽅法:表示⽅法不可被⼦类覆盖,但是可以重载
- 修饰变量:表示变量⼀旦被赋值就不可以更改它的值。
修饰成员变量:
- 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
- 如果final修饰的是成员变量,可以在⾮静态初始化块、声明该变量或者构造器中执⾏初始值。
修饰局部变量:
- 系统不会为局部变量进⾏初始化,局部变量必须由程序员显示初始化。
- 因此使⽤final修饰局部变量时, 即可以在定义时指定默认值(后⾯的代码不能对变量再赋值),也可以不指定默认值,⽽在后⾯的代码 中对final变量赋初值(仅⼀次)
class FinalVar { final static int a = 0;//再声明的时候就需要赋值 或者静态代码块赋值 final int b = 0;//再声明的时候就需要赋值 或者代码块中赋值 或者构造器赋值 public static void main(String[] args) { final int localA; //局部变量只声明没有初始化,不会报错,与final⽆关。 localA = 0;//在使⽤之前⼀定要赋值 //localA = 1; 但是不允许第⼆次赋值 } }
修饰基本类型数据和引⽤类型数据:
- 如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;
- 如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。但是引⽤的值是可变 的。
public static void main() { final int[] iArr = {1, 2, 3, 4}; iArr[2] = -3;//合法 iArr = null;//⾮法,对iArr不能重新赋值 final Person p = new Person(25); p.setAge(24);//合法 p = null;//⾮法 }
为什么局部内部类和匿名内部类只能访问局部final变量?
⾸先需要知道的⼀点是: 内部类和外部类是处于同⼀个级别(类级别)的,内部类不会因为定义在⽅法中就会随着⽅法的执⾏完毕就被销毁。
- 这⾥就会产⽣问题:当外部类的⽅法结束时,局部变量就会被销毁了,但是内部类对象可能还存在。
- 这⾥就出现了⼀个⽭盾:内部类对象访问了⼀个不存在的变量。
为了解决这个问题,就将局部变量复制了⼀份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"复制体"。
- 将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和⽅法的局部变量的⼀致性。
class Test { //局部final变量a,b public void test(final int b) {//jdk8在这⾥做了优化, 不⽤写final final int a = 10; new Thread() {//匿名内部类,使用了a,b public void run() { //如果内部类使用了局部变量,这个局部变量必须是final System.out.println(a); System.out.println(b); } }.start(); } }
9. Java中的异常处理,你了解多少?
1. Java中的异常概念
在Java中,异常是一种程序运行中出现的非正常情况。当程序执行到某一步骤而出现无法处理的情况(如除数为零、文件未找到等),系统会创建一个异常对象并抛出该异常。Java的异常系统通过Throwable类来管理异常和错误,包含以下两大类:
1.1 Exception(异常)
Exception表示可以恢复的错误,通常由程序引发,建议通过捕获和处理来让程序继续执行。它主要分为两种:
检查异常
检查异常(Checked Exception):必须捕获和处理的异常,编译器要求在方法声明中使用throws关键字声明,或在方法内部通过try-catch捕获。
常见的检查异常包括:
- IOException:文件读写时可能出现,如文件未找到或无法读取。
- SQLException:数据库操作中的异常,例如查询失败。
- ClassNotFoundException:加载类失败时抛出,通常出现在反射相关的操作中。
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class CheckedExceptionDemo { public static void main(String[] args) { try { FileInputStream file = new FileInputStream(new File("test.txt")); // 文件可能不存在 } catch (IOException e) { System.out.println("捕获到IOException:" + e.getMessage()); } } }
示例:在上例中,FileInputStream构造方法可能会抛出IOException,所以编译器要求必须捕获或抛出这个异常。
运行时异常
运行时异常(RuntimeException):属于非检查异常(Unchecked Exception),编译器不强制要求捕获。尽管这些异常可以捕获和处理,但大多数情况下,我们应该通过改进代码来避免它们。
常见的运行时异常包括:
- NullPointerException:尝试访问空对象的属性或方法时抛出。
- ArrayIndexOutOfBoundsException:访问数组时索引超出数组的长度范围。
- ArithmeticException:除以零时抛出。
public class RuntimeExceptionDemo { public static void main(String[] args) { try { int result = 10 / 0; // 试图除以0,会导致ArithmeticException } catch (ArithmeticException e) { System.out.println("捕获到异常:" + e.getMessage()); } } }
示例:在这里,ArithmeticException是RuntimeException的子类。即使不捕获,程序也可以正常编译,但运行时会抛出异常。
1.2 Error(错误)
Error一般指程序无法控制的、无法恢复的严重错误。它们通常由Java运行时引发,表示系统层面的问题,如资源不足或系统故障,不建议捕获这些错误,因为它们代表的是程序之外的系统异常,通常无法恢复。
常见的错误包括:
- OutOfMemoryError:JVM内存不足时抛出。
- StackOverflowError:方法调用过深导致栈内存溢出,例如无限递归。
- InternalError:JVM内部错误,通常在特殊情况或底层故障时出现。
示例:
public class ErrorDemo { public static void main(String[] args) { try { int[] largeArray = new int[Integer.MAX_VALUE]; // 尝试创建超大的数组 } catch (OutOfMemoryError e) { System.out.println("捕获到内存不足错误:" + e.getMessage()); } } }
在这种情况下,虽然可以捕获OutOfMemoryError,但不推荐这样做,因为即便捕获到此错误,通常程序也已无法继续运行。
2. 捕获异常
捕获异常的主要目的是让程序在遇到异常时继续执行而不是直接终止。Java通过try-catch结构来捕获异常,并提供了finally块来执行一定的清理工作。捕获异常的关键字包括try、catch和finally。
- try:包含可能会抛出异常的代码块。
- catch:用于捕获并处理特定类型的异常。可以有多个catch块,分别处理不同类型的异常。捕获到的异常对象包含了异常的详细信息,方便程序员了解异常的原因。
- finally:无论是否发生异常都会执行的代码块,常用于释放资源(如关闭文件或数据库连接)。
2.1捕获异常的结构
使用try-catch-finally时,结构如下:
try { // 可能抛出异常的代码 } catch (异常类型1 e) { // 异常类型1的处理代码 } catch (异常类型2 e) { // 异常类型2的处理代码 } finally { // 总会执行的代码 }
示例代码
下面是一个捕获异常的示例代码,其中可能抛出ArrayIndexOutOfBoundsException和ArithmeticException。
public class CatchDemo { public static void main(String[] args) { int[] numbers = {10, 20}; try { int result = numbers[2] / 0; // 访问数组越界并除以零 } catch (ArrayIndexOutOfBoundsException e) { System.out.println("捕获到数组越界异常:" + e.getMessage()); } catch (ArithmeticException e) { System.out.println("捕获到算术异常:" + e.getMessage()); } finally { System.out.println("无论如何,都会执行finally块"); } } }
在上面的代码中:
- try块中包含两个异常风险:数组越界和除以零。
- 每个catch块捕获不同类型的异常,打印出异常信息。
- finally块中的代码总会执行,无论是否抛出异常。
2.2多个catch块的使用
Java支持在一个try语句后面添加多个catch块,从而对不同类型的异常进行不同的处理。值得注意的是,catch块中的异常类型从具体到通用排序更为合适,因为子类异常需要先于父类异常捕获。
try { // 可能抛出异常的代码 } catch (IOException e) { System.out.println("捕获到IO异常"); } catch (Exception e) { System.out.println("捕获到一般异常"); }
2.3使用finally释放资源
finally块的设计目的是确保关键的清理工作总会执行,比如关闭数据库连接、释放文件句柄等,即便在try块中出现了异常。
示例:
import java.io.FileInputStream; import java.io.IOException; public class FinallyDemo { public static void main(String[] args) { FileInputStream file = null; try { file = new FileInputStream("test.txt"); // 读取文件内容 } catch (IOException e) { System.out.println("文件读取错误:" + e.getMessage()); } finally { try { if (file != null) file.close(); // 确保资源释放 } catch (IOException e) { System.out.println("文件关闭错误:" + e.getMessage()); } } } }
在该示例中,finally确保了FileInputStream对象即便在读取过程中出现异常,也会被正确关闭,避免资源泄漏。
3. 抛出异常
在Java中,当方法遇到异常情况无法处理时,可以通过throw关
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
(1)全网最精简八股整理,各个头部公司最新面经整理(2)面试时非技术问题的话术整理;价格随着内容增加而增加,早订阅早享受