02_Java_口试实经_底子
02_Java_面试真经_基础
1、什么是反射
反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为反射机制。
2、深拷贝和浅拷贝区别是什么?
数据分为基本数据类型和引用数据类型。基本数据类型数据直接存储在栈中;引用数据类型存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。
- 浅拷贝对于基础数据类型直接复制数据值;对于引用数据类型只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。
- 深拷贝对于基础数据类型直接复制数据值;对于引用数据类型开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。
深拷贝相比于浅拷贝速度较慢并且花销较大。
为什么要使用克隆?
克隆的对象可能包含一些已经修改过的属性,而 ne 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠克隆方法了。
如何实现对象克隆?
- 实现 Cloneable 接口并重写 Object 类中的 clone() 方法。
- 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
深拷贝和浅拷贝区别是什么?
- 浅克隆当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
- 深克隆除了对象本身被复制外,对象所包含的所有成员变量也将复制。
3、并发和并行有什么区别?
并发两个或多个事件在同一时间间隔发生。
并行两个或者多个事件在同一时刻发生。
并行是真正意义上,同一时刻做多件事情,而并发在同一时刻只会做一件事件,只是可以将时间切碎,交替做多件事情。
4、内存溢出和内存泄漏有什么区别?
内存泄露:代码中的某个对象本应该被虚拟机回收,但因为拥有GCRoot引用而没有被回收。
内存溢出: 虚拟机由于堆中拥有太多不可回收对象没有回收,导致无法继续创建新对象。
5、构造器是否可被重写?
Constructor 不能被 override(重写),可以 overload(重载),所以你可以看到 个类中有多个构造函数的情况。
6、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
值传递。Java 中只有值传递,对于对象参数,值的内容是对象的引用。
7、Java 静态变量和成员变量的区别。
public class Demo {
/
静态变量又称类变量,static修饰
/
public static String STATIC_VARIABLE = "静态变量";
/
实例变量又称成员变量,没有static修饰
/
public String INSTANCE_VARIABLE = "实例变量";
}
成员变量存在于堆内存中。静态变量存在于方法区中。
成员变量与对象共存亡,随着对象创建而存在,随着对象被回收而释放。静态变量与类共存亡,随着类的加载而存在,随着类的卸载而消失。
成员变量所属于对象,所以也称为实例变量。静态变量所属于类,所以也称为类变量。
成员变量只能被对象所调用 。静态变量可以被对象调用,也可以被类名调用。
8、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
区分两种情况,发出调用时是否显示创建了对象实例。
(1) 没有显示创建对象实例不可以发起调用,非静态方法只能被对象所调用,静态方法可以通过对象调用,也可以通过类名调用,所以静态方法被调用时,可能还没有创建任何实例对象。通过静态方法内部发出对非静态方法的调用,此时可能无法知道非静态方法属于哪个对象。
public class Demo {
public static void staticMethod() {
// 直接调用非静态方法编译报错
instanceMethod();
}
public void instanceMethod() {
System.out.println("非静态方法");
}
}
(2)显示创建对象实例可以发起调用,在静态方法中显示的创建对象实例,则可以正常的调用。
public class Demo {
public static void staticMethod() {
// 先创建实例对象,再调用非静态方法成功执行
Demo demo = ne Demo();
demo.instanceMethod();
}
public void instanceMethod() {
System.out.println("非静态方法");
}
}
9、初始化考察,请指出下面程序的运行结果。
public class InitialTest {
public static void main(String[] args) {
A ab = ne B();
ab = ne B();
}
}
class A {
static { // 父类静态代码块
System.out.print("A");
}
public A() { // 父类构造器
System.out.print("a");
}
}
class B extends A {
static { // 子类静态代码块
System.out.print("B");
}
public B() { // 子类构造器
System.out.print("b");
}
}
执行结果ABabab,两个考察点
1)静态变量只会初始化(执行)一次。
2)当有父类时,完整的初始化顺序为父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器 ->子类非静态变量(非静态代码块)->子类构造器 。
B ab = ne B();
ab = ne B();
结果也是一样
注意创建子类对象调用子类的构造方法的时候会先调用父类的构造方法,在子类的构造方法中调用父类的构造方法是用super(),如果没有写super(),则默认调用父类的无参构造方法。
10、重载(Overload)和重写(Override)的区别?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载一个类中有多个同名的方法,具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。
- 参数类型、个数、顺序至少有一个不相同。
- 不能重载只有返回值不同的方法名。
- 存在于父类和子类、同类中。
重写发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,必须是父类返回值的派生类。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。
- 方法名、参数、返回值相同。
- 子类方法不能缩小父类方法的访问权限。
- 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
- 存在于父类和子类之间。
- 方法被定义为final不能被重写。
11、为什么不能根据返回类型来区分重载?
如果我们有两个方法如下,当我们调用test(1) 时,编译器无法确认要调用的是哪个。
// 方法1
int test(int a);
// 方法2
long test(int a);
方法的返回值只是作为方法运行之后的一个“状态”,并不是所有调用都关注返回值,所以不能将返回值作为重载的唯一区分条件。
12、抽象类(abstract class)和接口(interface)有什么区别?
抽象类只能单继承,接口可以多实现。
抽象类可以有构造方法,接口中不能有构造方法。
抽象类中可以有成员变量,接口中没有成员变量,只能有常量(默认就是 public static final)
抽象类中可以包含非抽象的方法,在 Java 7 之前接口中的所有方法都是抽象的,在 Java 8 之后,接口支持非抽象方法default 方法、静态方法等。Java 9 支持私有方法、私有静态方法。
抽象类中的抽象方法类型可以是任意修饰符,Java 8 之前接口中的方法只能是 public 类型,Java 9 支持 private 类型。
设计思想的区别
- 接口是自上而下的抽象过程,接口规范了某些行为,是对某一行为的抽象。我需要这个行为,我就去实现某个接口,具体这个行为怎么实现,完全由自己决定。
- 抽象类是自下而上的抽象过程,抽象类提供了通用实现,是对某一类事物的抽象。我们在写实现类的时候,发现某些实现类具有几乎相同的实现,我们将这些相同的实现抽取出来成为抽象类,然后如果有一些差异点,则可以提供抽象方法来支持自定义实现。
13、Error 和 Exception 有什么区别?
Error 和 Exception 都是 Throable 的子类,用于表示程序出现了不正常的情况。区别在于
- Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。
- Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。
14、Java 中的 final 关键字有哪些用法?
final成员变量表示常量,只能被赋值一次,赋值后值不再改变(final要求地址值不能改变)
14.1、final变量
当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。
final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
14.2、final方法
- 第一个原因是把方法锁定,以防任何继承类修改它的含义,不能被重写;
- 第二个原因是效率,final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
14.3、final类
当用final修饰一个类时,表明这个类不能被继承。,一个类不能被声明为abstract 和 final。
final类中的成员变量可以根据需要设为final,要注意final类中的所有成员方法都会被隐式地指定为final方法。
14.4、原理
对于final域,编译器和处理器要遵守两个重排序规则
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。(先写入final变量,后调用该对象引用)
原因编译器会在final域的写之后,插入一个StoreStore屏障(写屏障,用于将写屏障之前的值同步到内存中,禁止屏障前的代码与屏障后的代码进行重排序)
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。(先读对象的引用,后读final变量)
原因编译器会在读final域操作的前面插入一个LoadLoad屏障(读屏障,用于将读屏障后面的值从内存中读取,禁止屏障前的代码与屏障后的代码重排序)
示例解释1
public class FinalExample {
int i; // 普通变量
final int j; // final 变量
static FinalExample obj;
public void FinalExample() { // 构造函数
i = 1; // 写普通域
j = 2; // 写 final 域
}
public static void riter() { // 写线程 A 执行
obj = ne FinalExample();
}
public static void reader() { // 读线程 B 执行
FinalExample object = obj; // 读对象引用
int a = object.i; // 读普通域 a=1或者a=0或者直接报错i没有初始化
int b = object.j; // 读 final域 b=2
}
}
(1)第一种情况
写普通域的操作被编译器重排序到了构造函数之外,而写 final 域的操作,被写 final 域的重排序规则“限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。
写 final 域的重排序规则可以确保在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。
(2)第二种情况
读对象的普通域的操作被处理器重排序到读对象引用之前,而读 final 域的重排序规则会把读对象 final 域的操作“限定”在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。
读 final 域的重排序规则可以确保在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。
15、阐述final、finally、finalize 的区别。
其实是三个完全不相关的东西,只是长的有点像。。
final 如上所示。
finallyfinally 是对 Java 异常处理机制的最佳补充,通常配合 try、catch 使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁、数据库连接等资源,把资源释放方法放到 finally 中,可以大大降低程序出错的几率。
finalizeObject 中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。finalize()是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize机制现在已经不推荐使用,并且在JDK 9开始被标记为deprecated。,并添加新的 java.lang.ref.Cleaner,提供了更灵活和有效的方法来释放资源。
16、try、catch、finally 考察,请指出下面程序的运行结果。
public class TryDemo {
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。
finally 的基础用法,在 return 前会先执行 finally 语句块,所以是先输出 finally 里的 3,再输出 return 的 1。
17、try、catch、finally 考察2,请指出下面程序的运行结果。
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
try {
return 2;
} finally {
return 3;
}
}
}
执行结果3。
这题有点嫌疑,但也不难,try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
18、try、catch、finally 考察3,请指出下面程序的运行结果。
public class TryDemo {
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。
这边其实根据字节码可以很容易看出来,在进入 finally 之前,JVM 会使用 iload、istore 两个指令,将结果暂存,在最终返回时在通过 iload、ireturn 指令返回暂存的结果。
19、JDK1.8之后有哪些新特性?
(1)接口默认方法Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可
(2)Lambda 表达式和函数式接口Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中),使用 Lambda 表达式使代码更加简洁,也不要滥用,否则会有可读性等问题,《Effective Java》作者 Josh Bloch 建议使用 Lambda 表达式最好不要超过3行。
Stream API用函数式编程方式在集合类上进行复杂操作的工具,配合Lambda表达式可以方便的对集合进行处理。Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
(3)方法引用方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
(4)日期时间APIJava 8 引入了新的日期时间API改进了日期时间的管理。
(5)Optional 类著名的 NullPointerException 是引起系统失败最常见的原因。很久以前 Google Guava 项目引入了 Optional 作为解决空指针异常的一种方式,不赞成代码被 null 检查的代码污染,期望程序员写整洁的代码。受Google Guava的鼓励,Optional 现在是Java 8库的一部分。
新工具新的编译工具,如Nashorn引擎 jjs、 类依赖分析器 jdeps。
20、ait() 和sleep() 方法的区别
- 来源不同sleep() 来自 Thread 类,ait() 来自 Object 类。
- 对于同步锁的影响不同如果当前线程持有同步锁,那么 sleep 是不会让线程释放同步锁的。ait() 会释放同步锁,让其他线程进入 synchronized 代码块执行。
- 使用范围不同sleep() 可以在任何地方使用。ait() 只能在同步控制方法或者同步控制块里面使用,否则会抛 IllegalMonitorStateException。
- 恢复方式不同两者会暂停当前线程,在恢复上不太一样。sleep() 在时间到了之后会重新恢复;ait() 则需要其他线程调用同一锁对象的 notify()/nofityAll() 才能重新恢复。
21、线程的sleep() 方法和yield() 方法有什么区别?
线程执行 sleep() 方法后进入超时等待(TIMED_WAITING)状态,而执行 yield() 方法后进入就绪(READY)状态。
sleep() 方法给其他线程运行机会时不考虑线程的优先级,会给低优先级的线程运行的机会;yield() 方法只会给相同优先级或更高优先级的线程以运行的机会。
22、线程的join() 方法是干啥用的?
用于等待当前线程终止。如果一个线程A执行了 threadB.join() 语句,其含义是当前线程A等待 threadB 线程终止之后才从 threadB.join() 返回继续往下执行自己的代码。
23、编写多线程程序有几种实现方式?
通常来说,可以认为有三种方式1)继承 Thread 类;2)实现 Runnable 接口;3)实现 Callable 接口。
其中,Thread 其实也是实现了 Runable 接口。Runnable 和 Callable 的主要区别在于是否有返回值。
创建线程的实现原理只有一种线程创建方式
继承Thread和实现Runnable接口两种方式本身就是一种方式,通过创建Thread实例,然后调用start()方法来创建实例
我们先主要看一下Callable接口实现类的使用,我们具体看一下ExecutorService的submit()方法
在submit()方法中,将Callable实例封装成一个FutureTask实例,FutureTask实现了RunnableFuture接口,而RunnableFuture又实现了Runnable接口,也就是说封装后的FutureTask仍然只是一个任务实例,此时与线程并没有任何关系,真正建立关系是在execute()方法中【AbstractExecutorService.java】
public Future submit(Callable task) {
if (task == null) thro ne NullPointerException();
RunnableFuture ftask = neTaskFor(task);
execute(ftask);
return ftask;
}
execute()方法是线程池的核心方法,该方法在后面介绍线程池的文章中会对其进行详细介绍,现在我们主要看它的addWorker()方法,该方法就是去创建一个线程【ThreadPoolExecutor.java】
public void execute(Runnable mand) {
if (mand == null)
thro ne NullPointerException();
int c = ctl.get();
if (orkerCountOf(c) < corePoolSize) {
if (addWorker(mand, true))
return;
c = ctl.get();
}
……
}
在addWorker()方法中,会去创建一个Worker实例,而在Worker的构造方法中,会去创建一个Thread实例【ThreadPoolExecutor.java】
private boolean addWorker(Runnable firstTask, boolean core) {
……
= ne Worker(firstTask);
final Thread t = .thread;
……
}
会去拿到一个ThreadFactory实例,我们以DefaultThreadFactory为例,看下ne orker()方法的实现,就是去创建了一个Thread实例
【ThreadPoolExecutor.java】
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().neThread(this);
}
【DefaultThreadFactory.java】
public Thread neThread(Runnable r) {
Thread t = ne Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
从上面对Callable的分析,我们可以得出结论,所有创建线程的方式都可以归结为一种方式,那就是创建Thread实
当调用Thread实例的run()方法时,就是简单的去调用Runnable实例的run()方法,也与线程的创建没有关系,只是普通的方法调用
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AessControlContext a,
boolean inheritThreadLocals) {
……
this.target = target;
……
}
public void run() {
if (target != null) {
target.run();
}
}
下面我们看一下start()方法的源码,在start()方法中,会去调用本地方法start0(),这个方法才是真正去创建一个线程
public synchronized void start() {
if (threadStatus != 0)
thro ne IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throable ignore) {
}
}
}
private native void start0();
线程创建流程
在Thread初始化的时候,会去调用本地方法registerNatives(),这个方法的主要作用是绑定线程相关的本地方法和真正JVM方法之间的映射关系
public class Thread implements Runnable {
/ Make sure registerNatives is the first thing does. /
private static native void registerNatives();
static {
registerNatives();
}
}
JNINativeMethod中建立了JNI的映射关系
(1)创建线程
当Thread对象调用start0()本地方法时,会去调用JVM的JVM_StartThread()方法进行线程的创建的和启动,而在该方法中,会调用navite_thread = ne JavaThread(&thread_entry,sz)进行线程的创建。
在该方法中,会去调用操作系统的线程创建的方法,以X86的linux系统为例,会去调用create_thread()方法,而在该方法中又去调用pthread_create(),这个方法才是去真正的创建一个线程。
线程创建完成之后,一直处于初始化的状态,所以会一直进行阻塞,直到被唤醒
上面创建线程的过程都是在navite_thread = ne JavaThread(&thread_entry,sz)中进行的,这个方法会得到一个JavaThread对象,这是JVM的层面的线程对象,接下来,它需要与Java的Thread对象进行绑定native_thread ->prepare(jthread)
(2)启动线程
完成上面内核线程创建和绑定工作之后,开始执行创建的内核线程,执行thread_entry()方法,里面会去调用start()方法Thread:start(native_thread),接着就是去调用操作系统的start方法os::start_thread(thread),将线程状态设置为RUNNABLE状态
在JavaThread::run会去调用JavaThread::thread_main_inner,在thread_main_inner()方法中,会去执行this->entry_point()(this,this),调用到thread_entry()方法,在该方法中,会根据前面JVM的JavaThread与Java的Thread对象的绑定关系,去调用Theaad对象的run()方法,至此一个线程就完全创建完成并开始执行业务了。
注从上面线程创建的流程中可以看出,Java的线程属于内核级线程,完全基于操作系统线程模型来实现,JVM与操作系统之间采用一对一的线程模型实现。
24、Thread 调用start() 方法和调用run() 方法的区别
run()普通的方法调用,在主线程中执行,不会新建一个线程来执行。
start()新启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到 CPU 时间片,就开始执行 run() 方法。
25、线程的状态流转
一个线程可以处于以下状态之一
NEW新建尚未启动的线程处于此状态,没有调用 start() 方法。
RUNNABLE包含就绪(READY)和运行中(RUNNING)两种状态。线程调用 start() 方法会会进入就绪(READY)状态,等待获取 CPU 时间片。如果成功获取到 CPU 时间片,则会进入运行中(RUNNING)状态。
BLOCKED线程在进入同步方法/同步块(synchronized)时被阻塞,等待同步锁的线程处于此状态。
WAITING无限期等待另一个线程执行特定操作的线程处于此状态,需要被显示的唤醒,否则会一直等待下去。例如对于 Object.ait(),需要等待另一个线程执行 Object.notify() 或 Object.notifyAll();对于 Thread.join(),则需要等待指定的线程终止。
TIMED_WAITING在指定的时间内等待另一个线程执行某项操作的线程处于此状态。跟 WAITING 类似,区别在于该状态有超时时间参数,在超时时间到了后会自动唤醒,避免了无期限的等待。
TERMINATED执行完毕已经退出的线程处于此状态。
线程在给定的时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。