02_Java_面试真经_基础

1、什么是反射

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

2、深拷贝和浅拷贝区别是什么?

数据分为基本数据类型和引用数据类型基本数据类型:数据直接存储在栈中;引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。

深拷贝相比于浅拷贝速度较慢并且花销较大。

为什么要使用克隆?

克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠克隆方法了。

如何实现对象克隆?

深拷贝和浅拷贝区别是什么?

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 = new Demo();
            demo.instanceMethod();
      }
      public void instanceMethod() {
     			 System.out.println("非静态方法");
      }
}

9、初始化考察,请指出下面程序的运行结果。

public class InitialTest {
    public static void main(String[] args) {
        A ab = new B();
        ab = new 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 = new B();
ab = new B();
结果也是一样

注意:创建子类对象调用子类的构造方法的时候会先调用父类的构造方法,在子类的构造方法中调用父类的构造方法是用super(),如果没有写super(),则默认调用父类的无参构造方法。

10、重载(Overload)和重写(Override)的区别?

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

重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。

重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。

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 都是 Throwable 的子类,用于表示程序出现了不正常的情况。区别在于:

14、Java 中的 final 关键字有哪些用法?

final成员变量表示常量,只能被赋值一次,赋值后值不再改变(final要求地址值不能改变)

14.1、final变量

 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。

final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值

14.2、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 writer() { // 写线程 A 执行
   		 obj = new 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 如上所示。

finally:finally 是对 Java 异常处理机制的最佳补充,通常配合 try、catch 使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁、数据库连接等资源,把资源释放方法放到 finally 中,可以大大降低程序出错的几率。

finalize:Object 中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。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)日期时间API:Java 8 引入了新的日期时间API改进了日期时间的管理。

(5)Optional 类:著名的 NullPointerException 是引起系统失败最常见的原因。很久以前 Google Guava 项目引入了 Optional 作为解决空指针异常的一种方式,不赞成代码被 null 检查的代码污染,期望程序员写整洁的代码。受Google Guava的鼓励,Optional 现在是Java 8库的一部分。

新工具:新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器 jdeps。

20、wait() 和sleep() 方法的区别

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) throw new NullPointerException();
    RunnableFuture ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

execute()方法是线程池的核心方法,该方法在后面介绍线程池的文章中会对其进行详细介绍,现在我们主要看它的addWorker()方法,该方法就是去创建一个线程【ThreadPoolExecutor.java】

public void execute(Runnable command) {
    if (command == null)
  		  throw new NullPointerException();
  	int c = ctl.get();
  	if (workerCountOf(c) < corePoolSize) {
      if (addWorker(command, true))
     		 return;
      c = ctl.get();
    }
    ……
}

在addWorker()方法中,会去创建一个Worker实例,而在Worker的构造方法中,会去创建一个Thread实例【ThreadPoolExecutor.java】

private boolean addWorker(Runnable firstTask, boolean core) {
……
w = new Worker(firstTask);
final Thread t = w.thread;
……
}

首先会去拿到一个ThreadFactory实例,我们以DefaultThreadFactory为例,看下new worker()方法的实现,就是去创建了一个Thread实例

【ThreadPoolExecutor.java】

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

【DefaultThreadFactory.java】

public Thread newThread(Runnable r) {
    Thread t = new 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, AccessControlContext acc,
    boolean inheritThreadLocals) {
        ……

        this.target = target;
        ……
    }

    public void run() {
        if (target != null) {
      		  target.run();
        }
}

下面我们看一下start()方法的源码,在start()方法中,会去调用本地方法start0(),这个方法才是真正去创建一个线程

public synchronized void start() {
    if (threadStatus != 0)
    		throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
            group.threadStartFailed(this);
            }
        } catch (Throwable 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 = new JavaThread(&thread_entry,sz)进行线程的创建。

在该方法中,会去调用操作系统的线程创建的方法,以X86的linux系统为例,会去调用create_thread()方法,而在该方法中又去调用pthread_create(),这个方法才是去真正的创建一个线程。

线程创建完成之后,一直处于初始化的状态,所以会一直进行阻塞,直到被唤醒

上面创建线程的过程都是在navite_thread = new 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.wait(),需要等待另一个线程执行 Object.notify() 或 Object.notifyAll();对于 Thread.join(),则需要等待指定的线程终止。

TIMED_WAITING:在指定的时间内等待另一个线程执行某项操作的线程处于此状态。跟 WAITING 类似,区别在于该状态有超时时间参数,在超时时间到了后会自动唤醒,避免了无期限的等待。

TERMINATED:执行完毕已经退出的线程处于此状态。

线程在给定的时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。

展开阅读全文

页面更新:2024-04-02

标签:子类   真经   初始化   线程   变量   静态   接口   对象   状态   代码   基础   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top