Java基础,面试整理(二)

本文深入探讨Java高级特性,包括锁机制、并发控制、内存管理、异常处理、线程同步、垃圾回收、对象克隆等核心主题,旨在提升开发者对Java底层机制的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java基础(二)

1、Synchronized和lock的比较

  • Synchronized是Java的关键字,是内置的语言实现;Lock是一个接口。
  • 发生异常时,Synchronized会默认自动释放线程占有的锁;而Lock需要主动释放,可通过unlock()去释放,否则容易出现死锁现象,因此使用Lock时需要在finally块中释放锁。
  • Synchronized无法让等待的线程响应中断;而Lock却可以中断。
  • Synchronized无法判断是否成功获取锁;而Lock可以。
  • Lock可以提高多个线程进行读操作的效率。

2、Java锁与Synchronized对象锁(方法锁)、类锁的区别

  • java的内置锁
  • 每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
  • java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
  • Synchronized对象锁(方法锁)和类锁
    synchronized称为”同步锁“,它在修饰代码块的时候需要传入一个引用对象作为“锁”的对象。
  • Sychronized在修饰成员方法的时候,默认是当前对象作为锁的对象,需要的是对象锁或方法锁
  • 在修饰静态方法的类时,默认是当前类的Class对象作为锁的对象,需要的是类锁
  • java的对象锁和类锁在锁的概念上基本上和java内置锁是一致的,但是,两个锁实际是有很大的区别的。
  • 对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
  • 无论是类锁还是对象锁,父类和子类之间是否阻塞没有直接关系。当子类覆写父类中的同步方法或是接口中声明的同步方法的时候,synchronized修饰符是不会被自动继承的,所以相应的阻塞问题不会出现。

3、volitile的介绍

volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。

  • volatile 的特性
  1. 可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  2. 有序性:禁止进行指令重排序。(实现有序性)
  3. volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
    详细资料可参考:Volatile详细了解

4、final关键字的用法

  1. 修饰类:表示该类不能被继承;
  2. 修饰方法:表示该方法不能被重写;
  3. 修饰变量:表示该变量只能被赋值一次,以后只能读取不能修改,且声明的时候必须赋初始值。

5、JVM加载class文件的原理机制

JVM中的类的装载是由类加载器(ClassLoader)和它的子类来实现的,类加载器负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)、初始化

  1. 加载阶段:类的加载指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不能用。
  2. 连接阶段:当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。
  3. 初始化阶段:完成上面两个阶段后,最后JVM对类进行初始化,包括: 1)、如果类存在直接的父类并且类没有被初始化,那么先初始化父类;2)、如果类中存在初始化语句,就依次执行这些初始化语句。
    其中,类的加载由类的加载器完成,类加载器包括:根加载器(BootStrap)、扩展加载器(Ectension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。
    (1)、BootStrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
    (2)、Extension:它的父加载器是 Bootstrap;
    (3)、System:又叫应用类加载器,其父类是 Extension。

6、char型变量能不能存储一个中文汉字?Why?

char类型可以存储一个中文汉字。因为Java使用的编码是Unicode,一个char类型占2个字节(16 bit),所以存放一个汉字是没有问题的。
【补充】使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符从JVM内部转移到外部时,需要进行编码转换。所以Java中有字符流和字节流,以及在这两者之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类时字节流和字符流之间的适配器类,承担了编码转换的任务。

7、抽象类和接口的异同

抽象类:一个类中如果有抽象方法,则这个类就是抽象类。抽象类的子类腰围父类中的所有抽象方法提供具体的实现,如果不实现某个抽象方法的话,就在子类中继续声明这个方法是抽象的,也就是说该子类还是抽象类。

接口:表示一个方法的集合。接口中的所有方法都没有方法体,接口中的所有方法都是抽象的,接口里的成员变量默认也都是static final类型。由于抽象类可以包含部分方法的实现,所以在一些场合下抽象类比接口存在更多的优势。

两者相同之处:

  1. 抽象类和接口都不能够实例化,但可定义抽象类和接口类型的引用。
  2. 一个类如果继承了某个抽象类或者实现了某个接口,需要实现其中的所有抽象方法,这样才能被实例化。

两者的不同之处:

  • 抽象类中可以定义构造器,可以有包含抽象方法和具体方法;而接口中不能定义构造器且其中的方法必须全是抽象方法。
  • 抽象类只能被继承,而接口可以被实现。一个类可以实现多个接口,但一个类只能继承一个抽象类。
  • 抽象类的成员变量可以是private、默认、protected、public;而接口的成员变量默认为public。
  • 抽象类中可以定义成员变量;而接口中定义的成员变量实际上都是常量。
  • 抽象类强调所属关系,其设计理念为“is-a”关系(xx是xx);而接口强调特定功能的实现,其设计理念为“has-a”关系(xx具体xx)

【补充】接口可以继承接口,而且支持多重继承。抽象类可以实现接口,抽象类既可以继承具体类也可以继承抽象类。

8、静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?

静态嵌套类是被声明为静态的内部类,它不依赖于外部类的实例化情况,不管外部类是否实例化,它都能被实例化。
而通常的内部类需要外部类被实例化后,才能够被实例化。

9、Java中会存在内存泄露吗?

理论上,因为Java有垃圾回收机制(GC),所以不会存在内存泄露问题,但是在实际开发中,可能会存在无用但可达的对象,这些对象不会被GC回收,因此也会导致内存泄露的发生。例如:Hibernate的Session(一级缓存)中的对象属于持久态,不会被GC回收,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭和清空一级缓存就可能导致内存泄露的发生。

10、静态变量和实例变量的区别

  1. 语法上的区别:静态变量需使用static修饰符进行修饰;而实例变量不需要。
  2. 运行时的区别:静态变量属于类,它也称为类变量,不属于类的任何一个对象,一个类不管创建了多少个对象,静态变量在内存中有且仅有一个拷贝,只要程序加载了该类的字节码,就可直接使用。实例变量属于对象的属性,必须创建了实例对象(如new)才会被分配空间,才可以使用实例变量。
  3. 总之,静态变量可以直接使用类名来引用,而实例变量必须创建对象后才可以通过这个对象来使用。
public class BainLiang {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //直接调用测试:
        System.out.println(QuBie.staticInt);	//0
        // System.out.println(QuBie.shiInt);	//报错
        QuBie qb1 = new QuBie();	//静态变量1	 实例变量1
        QuBie qb2 = new QuBie();	//静态变量2	 实例变量1
        qb1.shiInt = 8;
        qb1.staticInt = 8;//这里改变以后,再创建的对象也是会用改过的数值
        QuBie qb3 = new QuBie();	//静态变量9	 实例变量1
    }
}

class QuBie {
    public static int staticInt = 0;
    public int shiInt = 0;

    public QuBie() {
        staticInt++;
        shiInt++;
        System.out.println("静态变量" + staticInt + "   实例变量" + shiInt);
    }
}

11、可以使用静态方法内部调用非静态方法?

不可以,静态方法只能访问静态成员,而非静态方法的调用需要先创建对象,在调用静态方法时对象可能没有初始化。

12、如何实现对象克隆?

  1. 实现Cloneable接口并重写Object类中的clone()方法;
  2. 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

13、GC的介绍

Java提供的GC功能可以自动检测对象是否超过作用域从而达到自动回收内存的目的。Java程序员不用担心内存管理,垃圾收集器会自动进行管理。要求垃圾收集,可以调用System.gc()或Runtime.getRuntime.gc(),但JVM可以屏蔽显示的垃圾回收调用。

垃圾回收机制分为新生代和老年代,具体区域分为以下:
1)、Eden(伊甸园):这是对象最初诞生的区域,并且对大多数对象来说,这是它们唯一存在过的区域。
2)、Survivor(幸存者乐园):从Eden幸存下来的对象会挪到这个区域中,又分为幸存区S0和幸存区S1。
3)、Tenured(终身颐养院):足够老的幸存对象的存放区域。

垃圾回收机制设计的算法主要有:

  1. 引用计数法:给对象添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可再被使用的对象,可当做垃圾进行回收。
  2. 可达性分析:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。(其中虚拟机栈中的引用对象、方法区中的常量引用对象、方法区中的类静态属性引用对象、本地方法栈中的引用对象和活跃线程中的引用对象都可以作为GC Root)
  3. 标记-清除算法:该算法分为“标记”和“清除”两个阶段:标记阶段的任务是从根集合进行扫描,标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
  4. 复制算法:为了解决效率问题,开发出了复制算法。它可以将内存分为大小相同的两块,每次使用其中的一块。当第一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
  5. 标记-整理算法:为了解决复制算法的缺陷,充分利用内存空间,提出了标记整理算法。该算法标记阶段和标记清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
  6. 分代收集算法:当前虚拟机的垃圾收集都采用分代收集算法,这种算法就是根据具体的情况选择具体的垃圾回收算法。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
算法优点缺点
标记-清除算法----效率低;产生大量不连续的碎片
复制算法(解决标记-清除算法的缺点)实现简单,运行高效内存空间利用不充分
标记-整理算法(解决复制算法的缺点)内存空间利用率高性能较低

14、匿名内部类

匿名内部类就是没有名字的内部类。正因如此,匿名内部类只能使用一次,它通常来简化代码编写。
使用匿名内部类需要注意的点:

  1. 匿名内部类必须继承一个父类或实现一个接口,必须实现父类或接口中所有抽象方法,也就是说匿名内部类中的方法不能是抽象的;
  2. 因匿名内部类没有类名,所以匿名内部类不能定义构造器
  3. 匿名内部类不能定义任何静态成员、方法;
  4. 匿名内部类可以访问外部类私有变量和方法。
  5. 匿名内部类可以访问外部类成员变量或成员方法,(包括私有成员变量),但它们必须用static修饰。

15、try{return语句;},后面紧跟finally{A}代码块,其里面的代码A会被在什么时候被执行?

A代码块会在方法返回调用者之前被执行。
在finally中改变返回值的做法是不可取的。因为如果存在finally代码块,try中的return语句不会立即返回调用者,而是记录下返回值待finally代码块执行完毕之后再想调用者返回其值,然后如果在finallu中修改了返回值,就会返回修改后的值。

16、 Java如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?

在Java中,每个异常都是一个对象,它是Throwable类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。
try:用来指定一块预防所有异常的程序;
catch:catch子句跟在try块后面,用来指定想要捕获的异常的类型;
throw:用来明确地抛出一个异常;
throws:用来声明一个方法可能抛出的各种异常;
finally:为确保一段代码不管发生什么异常情况都会被执行。
一般情况下使用try来执行一段代码,如果系统抛出(throw)一个异常处理,可以通过它的类型来捕获(catch)它,或者通过总是执行代码块(finally)来处理。

17、Error和Exception的区别

  1. Error表示的是系统级别的错误以及程序不必处理的异常,是一种可以修复但是很难修复的一种情况,例如内存溢出。
  2. Exception是一种设计或实现的问题,表示需要程序进行处理的异常。

18、运行时异常和检查性异常(受检异常)的异同

异常表示程序运行过程中出现的非正常状态。

  • 运行时异常是一种常见的运行错误,只要程序设计无误,通常不会发生。运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 受检异常更程序运行的上下文相关,即使程序设计无误,仍然可能因使用的问题而发生。最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
    在这里插入图片描述
    Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。

【常见的运行时异常】

  • ArithmeticException(算术异常)
  • ClassCastException(类转换异常)
  • IllegalArgumentException(非法参数异常)
  • IndexOutOfBoundsException(下标越界异常)
  • NullPointerException(空指针异常)
  • SecurityException(安全异常)

19、final、finally、finalize的区别

  1. final见上述的第4点
  2. finally见第16点
  3. finalize:Object 类中定义的方法,是一种在对象被回收之前调用的方法,给对象自己最后一个复活的机会,但是什么时候调用 finalize 没有保证。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。

20、面向对象的特征

面向对象的特征

21、基本数据类型和包装类型

Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型。为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型。
【以int和Integer为例】:

class AutoUnboxingTest {
	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
		比较
	}
}

22、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,两者的区别?

  • sleep()方法(休眠)是线程类(Thread)的静态方法,调用次方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的所依然保持,因此休眠时间结束后会自动回复。
  • wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象等待池(wait pool),只有调用notify()(或notifyAll())时才能唤醒等待池中的线程进行等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

23、线程的sleep()和yield()的区别

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同或更高优先级的线程以运行的机会。
  2. 线程执行sleep()后会转入阻塞(blocked)状态;执行yield()后会转入就绪(ready)状态。
  3. sleep()声明抛出InterruptedException;yield()没有声明任何异常。
  4. sleep()比yield()方法具有更好的可移植性。

24、与线程同步以及线程调度相关的方法

  • wait():使一个线程处于等待(阻塞)状态,并释放所持有的对象的锁。
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法。
  • notify():中唤醒一个处于等待状态的线程,不能确切地唤醒某一个等待的线程,有JVM决定,且与优先级无关。
  • notifyAll():唤醒所有处于等待状态的线程,然后被唤醒的线程争夺对象的锁,获得锁的线程才能进入就绪状态。

25、编写多线程程序的实现方式

在Java5前实现多线程有两种方法:1)继承Thread类;2)实现Runnable接口。两种方法都要通过重写run()方法来定义线程的行为。由于Java中的继承是单继承的,一个类只有一个父类,如果继承了Thread类就无法继承其它类了,显然使用Runnable接口更为灵活。

在Java5之后,出现第三种方法,即实现Callable和Future接口,该接口中的call()方法可以在线程执行结束时产生一个返回值,也可以声明抛出异常。Java5提供了Future接口来代表Callable接口里的call()方法的返回值。

【三种方式的对比】

  1. 采用实现Runnable、Callable接口的方式创建线程时,线程只实现了Runnable接口或Callable接口,还可以继承其它类。
  2. 使用继承Thread类的方式创建多线程时,编写简单,若需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

26、一个线程的生命周期

线程的生命周期

27、线程池介绍

在面向对象中,创建和销毁对象很耗费资源,而JVM为了能够在对象销毁后进行垃圾回收,会试图跟踪每一个对象,这样会消耗大量资源,因此“池化资源”技术就应运而生。
线程池指事先创建若干个可执行的线程放入一个池(容器)中,需要时则从池中获取而不用自行创建,使用完毕后也不需要销毁而是放回池中,从而减少了创建和销毁线程对象的开销。

28、JVM介绍

JVM包含三大模块:类装载子系统、运行时数据区(内存模型)、字节码执行引擎。其中内存模型又分为五大子模块:堆、栈、本地方法栈、方法区(元空间)、程序计数器。
在这里插入图片描述

  1. 方法区:主要用来存放已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等。
  2. :虚拟机启动时创建,存放对象实例,几乎所有对象、数组等都是在此分配内存的。在JVM内存中占的比例最大,也是GC垃圾回收的主要阵地,在堆中,又分为新生代及老年代,新生代中又分三个区域,分别是Eden,Survivor To,Survivor From。
  3. 栈(虚拟机栈):当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等,方法开始执行前就先创建栈帧入栈,执行完后就出栈
  4. 本地方法栈:和虚拟机栈类似,不过区别是专门提供给Native方法用的。
  5. 程序计数器:占用很小的一片区域,我们知道JVM执行代码是一行一行执行字节码,所以需要一个计数器来记录当前执行的行数。

29、Java中堆和栈的区别

  1. JVM中的堆和栈属于不同的内存区域,使用目的也不同。
  2. 栈存储的是方法帧和局部变量,而堆存储的是实体。
  3. 栈的更新速度快于堆,因为局部变量的生命周期很短。
  4. 栈通常都比堆小,堆被整个JVM的所有线程共享,而栈属于私有线程。
  5. 栈存放的变量生命周期一旦结束就会被释放,而堆存放的实体会被垃圾回收机制不定时的回收。

30、Java 中的编译期常量是什么?使用它有什么风险?

  • 编译期常量指的是程序在编译时就能确定这个常量的具体值。
  • 非编译期常量指的是程序在运行时才能确定常量的值,也称为运行时常量。

从定义上看,声明为final类型的基本类型或String类型并赋值(非运算)的变量就是编译时变量。

// 编译期常量
final int a = 111;
final String str1 = "hello";

// 非编译期常量
final String str2 = new String("word");

由于编译期常量在编译时就确定了值,因此使用编译期常量的地方在编译时就会被替换成相应的值。

实际上我们一般将编译期常量声明为public static final类型(静态变量),在这种情况下,引用编译期常量不会导致类的初始化才是正确的。本来引用static变量会引起类加载器加载常量所在类并进行初始化,但由于是编译期常量,使得编译器在编译引用这个常量的类时,会直接将产量替换为对应值,也就无需再去加载常量所在的类了。

因此,使用编译期常量的风险有:A定义了一个编译期常量,B类中使用了这个常量,两者都进行了编译。然后修改了A中常量的值,重新进行编译时,系统只会重新编译改动的A类,而旧代码B没有更新编译,这就会导致B中常量值与A中常量值的不一致。

对应到实际业务中,可能是我们的程序中使用了一个第三方库中共有的编译期常量时,如果对方更新了该常量的值,而我们随后只更新依赖的jar包,那么我们的程序中该常量就是旧值,就会产生隐患。为了避免这种情况,在更新依赖的jar文件时,应该重新编译我们的程序。

31、

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值