91.Java开发设计七大原则
在Java编程中,有一些基本的设计原则和模式,被广泛接受为最佳实践。以下是七个主要的Java开发原则:
单一职责原则(SRP)
一个类应该只有一个引起它变化的原因。
开放-封闭原则(OCP)
类应该是可扩展的,但是不可修改。
李氏替换原则(LSP)
子类型必须能够替换掉它们的基类型。
依赖倒置原则(DIP)
高层模块不应该依赖于低层模块,它们都应该依赖于抽象。
接口隔离原则(ISP)
类不应该实现它不需要的接口。
迪米特法则(LOD)
一个软件实体应当尽可能少地与其他实体发生相互作用。
合成复用原则(CARP)
优先使用对象的组合而不是类的继承。
以下是每个原则的简单Java代码示例:
单一职责原则:
public class MyClass { // 一些方法和属性,专注于一个职责 }
开放-封闭原则:
public abstract class MyBaseClass { // 实现某些功能 } public class MySubClass extends MyBaseClass { // 扩展功能,不修改基类 }
李氏替换原则:
public class MyBaseClass { // 实现某些功能 } public class MySubClass extends MyBaseClass { // 扩展功能,满足基类所有的要求 }
依赖倒置原则:
public interface MyInterface { // 定义一个接口 } public class MyClass implements MyInterface { // 实现接口 }
接口隔离原则:
public interface MyInterface { // 定义一个接口,只包含必要的方法 }
迪米特法则:
public class MyClass { // 方法和属性只对合作类公开 }
合成复用原则
public class MyBaseClass { // 实现某些功能 } public class MySubClass { private MyBaseClass baseClass; // 通过组合使用基类功能 }
这些原则有助于编写可维护、可扩展和可复用的代码。在实践中,开发者应该根据具体情况来判断哪些设计模式或实践最适合特定的代码结构。
92.简述什么是Java反射?
Java反射是一种在运行时自我检查和操作对象内部成员的机制。它允许程序在运行时构造任意一个类的对象,了解任意一个对象所属的类,以及判断任意一个类所具有的成员变量和方法。通过反射,程序可以在运行时动态地获取程序的信息,并调用对象的属性和方法。Java反射机制的主要功能包括:
在运行时判断任意一个对象所属的类:通过反射,可以确定一个对象的具体类型。
在运行时构造任意一个类的对象:即使类的构造函数是私有的,也可以通过反射来创建对象实例。
在运行时判断任意一个类所具有的成员变量和方法:可以查看类的所有成员变量和方法,包括私有成员。
在运行时调用任意一个对象的方法:通过反射,可以动态地调用对象的任何方法,包括私有方法。
Java反射机制的实现依赖于Class类,它是所有对象的超类,提供了对Java对象的结构和行为进行描述的功能。反射是Java语言的一个特性,它允许程序在运行时进行自我检查并且对内部的成员进行操作,从而实现了动态语言的特性。反射机制在框架设计和动态代理等方面有着广泛的应用,使得Java程序在运行时具有更大的灵活性和可扩展性。
93.简述为什么要使用克隆?如何实现对象克隆?深拷贝和浅拷贝区别是什么?
Java中使用克隆的主要原因是想要复制一个对象,同时保留原始对象以便进行后续操作。克隆操作允许开发者创建一个与原始对象具有相同状态的新对象,而不需要重新初始化或重新设置对象的所有属性。这种机制在多种场景下非常有用,例如,当需要修改对象但又不想影响原始对象时,或者当需要创建对象的多个独立实例时。
(1)实现对象克隆的方法主要有两种:
实现Cloneable接口并重写clone方法:通过实现Cloneable接口并重写clone()方法,可以在子类中定义如何复制对象。这种方式通常实现的是浅拷贝,即只复制对象本身而不复制其引用的对象。
通过序列化和反序列化实现深拷贝:通过实现Serializable接口,对象的序列化和反序列化可以创建一个新的对象,这种方式可以实现深拷贝,即复制对象本身以及它引用的所有对象。
(2)深拷贝和浅拷贝的区别在于:
浅拷贝:只复制对象的基本属性,如果对象中包含引用类型的属性,那么复制的只是引用而不是实际的对象。因此,原始对象和拷贝对象可能会共享这些引用类型的属性,修改其中一个可能会影响到另一个。
深拷贝:复制对象本身以及它引用的所有对象。这意味着如果原始对象或拷贝对象的引用类型属性被修改,这种修改不会影响到另一个对象。深拷贝通常通过序列化和反序列化实现,或者通过手动复制引用类型的属性来实现。
总的来说,Java中的克隆机制提供了一种灵活的方式来处理对象的复制问题,而深拷贝和浅拷贝的选择取决于具体的应用场景和需求。浅拷贝适用于只需要复制基本数据类型和简单引用的情况,而深拷贝则适用于需要完全独立副本的复杂对象。
94.简述列举Java常见的异常有哪些?
Java中常见的异常类型包括:
(1)NullPointerException:当尝试访问一个null对象的属性或方法时抛出,这是一个运行时异常,需要程序员进行捕获和 处理。
(2)ArrayIndexOutOfBoundsException:当尝试访问数组中不存在的元素时抛出,这也是一个运行时异常。
(3)ArithmeticException:在进行数学运算时出现错误,如除数为0,会抛出此异常。
(4)FileNotFoundException:当尝试打开一个不存在的文件时抛出。
(5)RuntimeException:这是一种非常常见的异常类型,包括NullPointerException和ArrayIndexOutOfBoundsException 等,通常由程序逻辑错误引起。
(6)OutOfMemoryError:当内存不足时抛出,这是由于需要分配的对象的内存超出了当前最大的堆内存。
(7)IOException:在进行IO操作时,如读写磁盘文件或网络内容时可能会遇到的异常。
(8)ClassNotFoundException:当在类路径下找不到指定的类时抛出。
(9)NoSuchMethodException 和 NoSuchFieldException:当程序试图通过反射访问不存在的方法或字段时抛出。
(10)EOFException:当在输入过程中遇到文件或流的结尾时抛出。
(11)InstantiationException:当试图通过Class对象创建实例但无法通过构造器创建对象时抛出。
(12)InterruptedException:当线程处于等待、休眠或其他暂停状态,并被其他线程中断时抛出。
(13)CloneNotSupportedException:当对象不支持克隆操作时抛出。
(14)OutOfMemoryError:当内存不足,无法为对象分配内存时抛出。
(15)NoClassDefFoundException:当Java虚拟机或类加载器试图实例化某个类,但找不到该类的定义时抛出。
了解这些常见的异常类型及其处理方法,可以帮助程序员编写更加健壮和可靠的代码,确保程序的稳定性和可维护性
95.Java 中操作字符串都有哪些类?它们之间有什么区别?
在Java中,操作字符串的类主要包括String、StringBuffer和StringBuilder。这些类各有特点,适用于不同的场景。
String类:String类是Java中最常用的字符串类,用于创建和操作不可变的字符串。String对象一旦创建就不能被修改,每次对字符串的操作都会返回一个新的String对象。这种不可变性使得String对象在多线程环境中是安全的,但每次修改都需要创建新的对象,可能会带来性能开销。
StringBuffer类:StringBuffer类用于创建和操作可变的字符串,且是线程安全的。这意味着它的方法(如append和insert)可以在多个线程中同时访问而不会出现数据不一致的问题。StringBuffer类的方法是同步的,适用于需要在多线程环境下进行字符串操作的情况。
StringBuilder类:StringBuilder类也是用于创建和操作可变的字符串,但与StringBuffer不同的是,它不是线程安全的。StringBuilder的性能通常优于StringBuffer,因为它不需要进行同步操作,适用于单线程环境下的字符串拼接和修改操作。StringBuilder对象通过修改内部字符数组来实现内容的动态改变,从而节省空间和时间开销。
总结来说,选择使用哪个类取决于你的应用是否需要在多线程环境中操作字符串以及性能要求。如果字符串内容不需要频繁改变,且在多线程环境中使用,应选择String类;如果需要频繁修改字符串且在单线程环境中使用,应选择StringBuilder类;如果需要在多线程环境中频繁修改字符串,则应选择StringBuffer类。
96.简述Java 中都有哪些引用类型?
Java中的引用类型主要包括类对象、接口对象、数组对象、字符串对象、日期对象、集合框架对象、输入/输出流对象,以及四种类型的引用:强引用、软引用、弱引用和虚引用。
类对象:类是Java编程语言中引用类型的基础,它是一种模板,描述了一组具有相同属性和行为的对象。通过类的实例化,可以创建对象,并通过对象访问类的属性和方法。
接口对象:接口定义了一组方法的规范,但不包含方法的实现。类可以实现一个或多个接口,从而继承接口中定义的方法规范。接口的主要作用是定义一种标准,使得不同的类可以按照这种标准来实现相同的功能。
数组对象:数组是用于存储相同类型元素的引用类型,在内存中占据连续的空间,可以通过索引访问数组中的元素。数组的长度在创建时确定,之后不能改变。
字符串对象:字符串对象存储不可变字符序列,是Java中的一个基本数据类型String的实例化表示。
其他对象:Java中还有其他引用类型,用于表示特定用途的对象,例如日期对象(java.util.Date)、集合框架对象(如java.util.List、java.util.Map)、输入/输出流对象(如java.io.InputStream、java.io.OutputStream)等。
引用类型的四种类型:
强引用(StrongReference):这是最常见的引用类型,如果内存空间足够,则垃圾收集器永远不会回收被强引用对象。
软引用(SoftReference):软引用用于实现内存敏感的缓存。当系统将要发生内存溢出异常前,会回收软引用对象。
弱引用(WeakReference):弱引用也是用来实现内存敏感的缓存,但强度低于软引用,它不能阻止其引用的对象被垃圾收集器回收。
虚引用(PhantomReference):虚引用主要用来跟踪对象的销毁,它不会阻止垃圾收集器回收其引用的对象。
这些引用类型提供了灵活的内存管理策略,使得Java程序能够更有效地利用内存资源。
97.简述Java Bean的命名规范 ?
Java Bean的命名规范主要包括以下几个方面:
- 名称必须以字母开头,不能以数字或其他符号开头。
- 名称需使用驼峰命名法,即除首个单词以外的每个单词首字母大写。例如,studentName、address。
- 名称应该有意义,能够描述Bean所代表的含义,选择名称应尽量准确、简短明了。
- 名称不能包含空格、点、逗号等特殊字符。
- 名称不能与Java类型、关键字、类库中已有的类或方法名相同。
- 布尔类型的Bean属性使用is开头,例如:isAvailable()。
- Bean类需要使用默认构造函数无参,即public 类名(){},否则在一些框架中无法通过反射实例化对象。
- Bean的属性通常应该设置为私有的,通过公共的getter和setter方法进行访问,以保证属性的封装和数据的安全性。
- 遵循上述命名规范可以使Bean在使用时更加方便和清晰,也有助于提高代码的可读性和可维护性。
98.请Java Bean 属性命名规范问题分析 ?
Java Bean 属性命名规范通常遵循驼峰命名法(Camel Case),即除了第一个单词之外,所有单词的首字母都大写。当需要通过反射或者某些框架工具自动获取Java Bean的属性时,属性的访问方法需要遵循一定的命名规范。
Java Bean规范定义了属性访问方法的命名规则:
对于布尔类型的属性,命名规则是 isPropertyName。
对于其他类型的属性,命名规则是 getPropertyName 和 setPropertyName。
其中,PropertyName 是按照驼峰命名法编写的属性名。
例如,一个Java Bean类可能包含一个名为 firstName 的字符串属性,它的访问方法将会是:
public class Person { private String firstName; // 获取 firstName 属性的方法 public String getFirstName() { return firstName; } // 设置 firstName 属性的方法 public void setFirstName(String firstName) { this.firstName = firstName; } }
如果属性名以 is 开头,例如 isActive,它可能表示一个布尔类型的属性,它的访问方法将会是:
public class Person { private boolean isActive; // 获取 isActive 属性的方法 public boolean isActive() { return isActive; } // 设置 isActive 属性的方法 public void setActive(boolean isActive) { this.isActive = isActive; } }
遵循这些命名规范可以确保Java Bean属性可以被框架正确地访问和修改。如果命名不符合规范,可能会导致属性无法被正确处理,这是使用反射或者某些框架时需要注意的问题。
总结:
(1). javabean属性命名尽量使用常规的驼峰式命名规则
(2). 属性名第一个单词尽量避免使用一个字母:如eBook, eMail。
(3). boolean属性名避免使用 “is” 开头的名称
(4). 随着jdk, eclipse, spring 等软件版本的不断提高,底版本的出现的问题可能在高版本中解决了,低版本原来正常的代码可能在高版本环境下不再支持。
99.Java中为什么代码会重排序?
在Java中,代码重排是指编译器和处理器为了优化程序性能,可能会调整代码的执行顺序。这种重排可能会在不改变单线程程序语义的前提下发生。
重排可能会导致如下问题:
线程安全问题:如果存在数据依赖关系,重排可能会破坏这些依赖关系,导致程序行为不正确。
内存可见性问题:一个线程对共享变量的修改对另一个线程不一定是立即可见的。
为了防止重排,你可以使用volatile关键字,它可以防止重排,确保每次读都能看到最新的值,并且禁止把volatile字段之后的指令重排到其前面。
示例代码:
class VolatileExample { volatile boolean flag; void init() { flag = false; } void print() { while (!flag) { // 循环等待flag变为true } System.out.println("Flag is now true"); } void setFlag() { flag = true; // 当这行代码执行,其他线程能立即看到flag的新值 } }
在这个例子中,使用volatile关键字可以确保即使在setFlag方法后的代码被重排,其他线程也能立即看到flag的新值。
100.解释为什么都说Java反射慢,它到底慢在哪?
Java反射的性能问题主要源于动态类型检查、方法调用的开销、安全性检查、编译器优化限制以及自动拆装箱和遍历操作等因素。
动态类型检查:由于反射调用在编译时无法确定具体的方法,JIT编译器无法对这些代码进行静态分析和优化,这直接影响了反射的性能。
方法调用的开销:反射中频繁的自动拆装箱操作会导致应用性能下降。在反射中,由于在编译时不知道具体要调用的方法参数类型,需要用最通用的引用类型来处理所有参数,即Object。这导致JVM在运行时需要进行自动拆箱操作,将Object类型转换为实际的参数类型,从而增加了额外的开销。
安全性检查和权限验证:使用Java反射时,需要进行安全检查和权限验证,这也会增加额外的性能开销。
编译器优化限制:由于反射调用的代码在编译时无法进行静态分析和优化,这进一步降低了反射的性能。
自动拆装箱和遍历操作:在反射中,从方法数组中遍历查找需要调用的方法会增加反射调用的时间开销。这种遍历操作对于普通的方法调用来说是不需要的,从而增加了额外的性能开销。
综上所述,Java反射的性能问题主要源于其动态性和运行时才确定的操作特性,这些特性导致了额外的类型检查、安全性检查、自动拆装箱和遍历操作等开销,从而影响了反射的性能。
101.Java中的double和float变量有什么区别?
在Java中,double和float都是用来表示浮点数的数据类型,但它们之间存在一些关键的区别:
(1)精度和存储空间:
double是双精度浮点数,使用64位来表示一个浮点数,可以提供更高的精度和范围。它可以表示的数值范围大约是1.7E−3081.7E-3081.7E−308到1.7E+3081.7E+3081.7E+308之间的数。
float是单精度浮点数,使用32位来表示一个浮点数,因此精度较低。它可以表示的数值范围大约是1.4E−451.4E-451.4E−45到3.4E+383.4E+383.4E+38之间的数。
由于double使用的位数更多,它需要更多的存储空间。在Java中,double类型占用8个字节(64位),而float类型占用4个字节(32位)。
(2)默认类型:
在Java中,浮点数常量默认被视为double类型。例如,如果你写下double num = 3.14;,那么3.14会被当作double类型的常量。如果要将一个浮点数常量显式地指定为float类型,需要在数字后面加上f或F。例如,float num = 3.14f;。
(3)运算精度:
由于double具有更高的精度,因此在进行浮点数运算时,double类型的变量能够提供更准确的结果。当进行复杂的数学计算或需要高精度的数据时,使用double类型更为常见。
(4)字面值后缀:
在Java中,表示float类型的字面值需要在数字后面添加一个字母"f"或"F"来标识。而表示double类型的字面值可以直接写数字,也可以添加"d"或"D"后缀。
综上所述,选择使用double还是float取决于具体的应用需求,如果需要更高的精度和更大范围的数值表示,应选择double;如果对精度要求不高或者需要节省存储空间,可以考虑使用float类型。
102.简述列举Java中有哪些回调机制?
Java中的回调机制主要包括函数式接口回调、监听器模式回调、自定义回调接口回调以及Future和CompletableFuture回调。
函数式接口回调:Java 8引入了函数式接口的概念,这是一种只有一个抽象方法的接口。Lambda表达式和方法引用可以与函数式接口配合使用,实现简洁的回调机制。例如,Runnable和Callable就是典型的函数式接口,用于表示无返回值和有返回值的任务。通过传递这些接口的实例给线程池或其他并发框架,可以实现异步执行和回调的功能。
监听器模式回调:监听器模式是一种常见的事件处理机制,允许在特定事件发生时执行相应的回调函数。在Java中,通过定义监听器接口并在事件源类中维护一个监听器列表来实现这种模式。当事件发生时,事件源类会遍历监听器列表并调用每个监听器的回调方法。这种模式在GUI编程、网络编程等领域中广泛应用。
自定义回调接口回调:除了函数式接口和监听器模式外,还可以定义自定义的回调接口来实现回调机制。这些接口通常包含一个或多个方法,用于定义回调的行为。然后将这些接口的实例作为参数传递给需要执行回调的方法,当需要执行回调时,这些方法会通过接口实例来调用相应的方法。
Future和CompletableFuture回调:在Java的并发编程中,Future和CompletableFuture是处理异步操作结果的重要工具。Future表示一个异步计算的结果,允许在计算完成后获取结果或处理异常。而CompletableFuture是Java 8引入的,提供了更强大的异步编程能力,可以更好地处理异步操作的结果和异常。
这些机制提供了灵活的回调方式,使得程序能够根据不同的业务需求选择合适的回调策略,从而实现更高效的程序设计和执行。
103.Java中有哪些原子类?它们的原理分别是什么?
Java中的原子类主要包括AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference、AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray,以及AtomicStampedReference。这些类的原理基本上都是使用volatile关键字和CAS(Compare-and-Swap)算法来实现线程安全的原子操作。
AtomicBoolean:提供了原子的布尔操作。它通过使用volatile关键字和CAS算法来实现线程安全的布尔操作。CAS算法是一种乐观锁定的方式,通过比较当前值与期望值来判断是否需要更新,以避免使用传统的锁机制,从而提高并发性能。
AtomicInteger 和 AtomicLong:提供了原子的整数和长整数操作。它们内部封装了一个整型或长整型变量,并通过使用Unsafe类提供的CAS操作来实现原子更新。CAS是一种无锁的同步机制,它在更新值时会检查预期值是否与当前值相等,如果相等则更新为新值,否则不进行任何改变。同时,这些类还使用了volatile关键字来确保变量的内存可见性。
AtomicReference:提供了原子的引用类型操作。它封装了一个对象引用,并通过CAS操作来原子地更新引用指向的对象。compareAndSet方法会比较当前引用与预期引用是否一致,一致则替换为新的引用,否则不作更改。同样,volatile关键字确保了对象引用的可见性。
AtomicIntegerArray 和 AtomicLongArray:分别用于对整型数组和长整型数组进行原子操作。这两个类提供了对数组元素的原子更新操作,如getAndAdd、compareAndSet等。它们基于CAS机制在更新数组元素时确保原子性,同时使用volatile数组元素来保证多线程环境下的可见性。
AtomicStampedReference:带有标记(stamp)的引用类型原子操作类。除了维护一个引用外,还附加了一个整数标记。在进行原子更新时,不仅比较引用本身,还会比较标记值,以确保操作的原子性。
104.Java Switch是如何支持String的,为什么不支持long?
Java Switch支持String的方式是通过hashCode()函数,但不支持long类型的原因是因为long类型是一个64位的整数类型,而switch语句要求条件表达式是一个32位的整数类型。
Java中的switch语句在处理String类型时,实际上是利用了String类的hashCode()函数。由于hashCode()函数返回的是一个整数,switch语句通过这个整数来进行匹配。然而,当两个不同的字符串的hashCode()值相等时,就需要依靠equals()函数来确定它们是否真正相等。这种机制使得switch语句能够支持String类型的匹配。
因此,long类型无法直接用作switch语句的条件表达式。如果需要在switch语句中使用long类型的值,可以通过将其转换为int或其他适用的整数类型来实现。例如,可以使用类型转换将long类型转换为int,然后在switch语句中使用该int值。这种转换限制了switch语句直接支持long类型的能力。
此外,switch语句在处理枚举类型时,也是通过将其转换为int类型来进行判断的。这进一步说明了switch语句在处理非基本数据类型时,需要通过某种方式转换为基本数据类型(如int)来进行匹配。
105.简述float型float f=3.4是否正确?
在Java中,赋值语句float f = 3.4;是不正确的,因为字面常量3.4默认的类型是double,而变量f的类型是float。由于Java中浮点数的类型默认是double,如果要将一个浮点数赋值给一个float类型的变量,需要进行显式的类型转换。正确的写法应该是使用类型转换,如float f = (float) 3.4;或者在数字后面加上f或F来表示这是一个单精度浮点数,如float f = 3.4f;这样做可以明确指定这是一个单精度浮点数,从而避免类型不匹配的问题。
此外,需要注意的是,由于浮点数的精度问题,当对浮点数进行运算时,可能不会得到完全精确的结果。因此,在比较浮点数时,通常使用某种形式的容差比较来判断它们是否“足够接近”,而不是直接检查它们是否完全相等。
106.简述两个对象值相同(x.equals(y) == true),但却可有不同的hashcode,这句话对不对 ?
这句话不对。在Java中,如果两个对象x和y通过equals方法判断为相等(即x.equals(y) == true),那么根据Object类的规范,这两个对象的hashCode方法必须返回相同的整数值。
这是Object类中equals和hashCode方法之间的一个重要协定(contract):
如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一对象的hashCode方法都必须产生相同的整数结果。
如果两个对象根据equals(Object)方法是不相等的,那么调用这两个对象中任一对象的hashCode方法不要求一定产生不同的整数结果。但是,为不相等的对象生成不同整数结果可以提高哈希表的性能。
因此,如果x.equals(y) == true,那么x.hashCode()和y.hashCode()必须返回相同的值。如果它们返回不同的值,那么这违反了Object类的规范,并且可能导致在使用这些对象作为哈希表键时出现不一致的行为。
107.简述char型变量中能不能存贮一个中文汉字?为什么 ?
char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16比特),所以放一个中文是没问题的。
补充:使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务。
108.简述写 clone() 方法时,通常都有一行代码,是什么 ?
写 clone() 方法时,通常都有一行代码是 super.clone();。
在 Java 中,当你实现 clone() 方法时,通常会调用 super.clone() 来确保正确地复制对象。super.clone() 调用的是 Object 类的 clone() 方法,这个方法进行的是浅复制。浅复制意味着它复制对象中的所有字段,包括基本类型和对象引用,但不复制引用对象本身。这意味着,如果对象引用的是另一个对象,那么复制后的对象将引用与原始对象相同的另一个对象。
此外,super.clone() 的使用还涉及到对象的继承关系。在实现 clone() 方法时,首先需要把父类中的成员复制到位,然后才是复制自己的成员。这是因为 clone() 方法通常是在子类中覆盖父类的 clone() 方法实现的,而 super.clone() 负责产生正确大小的空间,并逐位复制父类的成员变量到子类对象中。
总的来说,super.clone(); 这行代码在实现 clone() 方法时是非常关键的,它确保了对象的正确复制,包括其父类的成员变量也被正确地复制到新的对象中。
109.Synchronized底层实现是什么?
Synchronized在Java中的底层实现是基于JVM的内置锁机制,具体是通过Monitor(监视器)来实现的。Monitor是一种同步机制,用于实现线程之间的互斥访问和协调。以下是synchronized底层实现的详细解释:
Monitor机制:
每个Java对象都有一个与之关联的Monitor。Monitor充当了一种互斥锁的角色,用于控制多个线程对共享资源的访问。
当一个线程想要访问某个对象的synchronized代码块时,它首先需要获取该对象的Monitor。如果该Monitor已经被其他线程持有,则当前线程将会被阻塞,直至Monitor变为可用状态。
当线程完成synchronized块的代码执行后,它会释放Monitor,并把Monitor返还给对象池,这样其他线程才能获取Monitor并进入synchronized代码块。
Monitor的具体实现:
在HotSpot虚拟机中,Monitor底层是由C++实现的,具体实现对象是ObjectMonitor。
ObjectMonitor结构体包含了多个字段,如_owner(标识拥有该monitor的线程)、_EntryList(存放未获取到锁资源处于阻塞状态的线程队列)、_WaitSet(存放处于wait状态的线程队列)等。
线程通过自旋CAS(Compare And Swap)的方式尝试获取锁。如果获取成功,则成为该锁的拥有者;如果失败,则进入_EntryList队列等待。
synchronized的字节码实现:
当synchronized修饰方法时,底层是通过使用一个ACC_SYNCHRONIZED关键字实现隐式的加锁与解锁。
当synchronized修饰代码块时,底层是通过monitorenter和monitorexit两个指令实现的加锁与解锁。执行同步代码之前使用monitorenter加锁,执行完同步代码后使用monitorexit释放锁。
锁升级:
从JDK 1.6开始,synchronized的实现机制进行了优化,引入了偏向锁、轻量级锁等,以减少锁的开销3。
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,但锁的升级是单向的。
综上所述,synchronized的底层实现依赖于JVM的Monitor机制,通过Monitor的加锁、解锁以及锁升级等机制,实现对共享资源的互斥访问。
110.short s1 = 1; s1 = s1 + 1;有错吗? short s1 = 1; s1 += 1;有错吗?
对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋 值给short型。而short s1 = 1; s1 += 1 ;
+=操作符会进行隐式自动类型转换,是 Java 语言规定的运算符; Java编译器会对它进行特殊处理,因此可以正确编译。因为s1+= 1;相当于s1 = (short)(s1 + 1)。
111.&和&&的区别?
(1)& :
按位与: 0 & 1 = 0 ; 0 & 0 = 0; 1 & 1 = 1
逻辑与: a == b & b ==c (即使a==b已经是 false了,程序还会继续判断b是否等于c)
(2)&&:
短路与:a== b && b== c (当a==b 为false则不会继续判断b是否等与c),比如判断某对象中的属性是否等于某值,则必须用&&,否则会出现空指针问题。
112.IntegerCache
public class IntegerTest { public static void main (String [] args ) { Integer a = 100 , b = 100 ,c = 129 ,d = 129; System.out .println(a==b ); System.out .println(c==d ); } }
结果
true false
来解释一下:
/** * Cache to support the object identity semantics of autoboxing for values between * -128 and 127 (inclusive) as required by JLS . * * The cache is initialized on first usage . The size of the cache * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option. * During VM initialization, java.lang.Integer.IntegerCache.high property * may be set and saved in the private system properties in the * sun .misc.VM class . */ private static class IntegerCache { static final int low = -128; static final int high ; static final Integer cache []; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue =sun .misc .VM .getSavedProperty("java.lang.Integer.IntegerCache.high"); if ( integerCacheHighPropValue != null ) { try { int i = parseInt(integerCacheHighPropValue); i = Math .max ( i , 127 ); // Maximum array size is Integer.MAX_VALUE h = Math .min ( i , Integer.MAX_VALUE - (-low) -1); } catch ( NumberFormatException nfe ) { // If the property cannot be parsed into an int, ignoreit . } } high = h; cache = new Integer[(high - low ) + 1 ]; int j = low; for ( int k = 0; k < cache . length ; k++) cache [ k ] = new Integer(j++ ); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} } public static Integer valueOf ( int i ) { assert IntegerCache.high >= 127; if ( i >= IntegerCache. low && i <= IntegerCache.high) return IntegerCache.cache [ i + (-IntegerCache.low )]; return new Integer(i ); }
通过源码,我们可以看出, -128~127之间做了缓存。考虑到高频数值的复用场景,这样做还是很合理 的,合理优化。最大边界可以通过-XX:AutoBoxCacheMax进行配置。
113.抽象类能使用 final 修饰吗?
不能。定义抽象类就是让其他类继承的,而 final修饰的类不能被继承。
114.static关键字
(1)抽象的 (abstract)方法是否可同时是静态的 (static)?
抽象方法将来是要被重写的,而静态方法是不能重写的,所以这个是错误的。 (2)是否可以从一个静态 (static)方法内部发出对非静态方法的调用?
不可以,静态方法只能访问静态成员,非静态方法的调用要先创建对象。
(3) static 可否用来修饰局部变量? static 不允许用来修饰局部变量
(4)内部类与静态内部类的区别?
静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果 要 访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变量、调用静态的方法;
普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方法。
如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访问属性或者调用方法。
如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性。如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即可。
(5) Java中是否可以覆盖(override) 一个private或者是static的方法?
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定 的。static方法跟类的任何实例都不相关,所以概念上不适用。
115.重载 (Overload)和重写 (Override )的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行 时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同 或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方 法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更 多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。
116.Collections.sort排序内部原理?
在Java 6中Arrays.sort()和Collections.sort()使用的是MergeSort,而在Java 7中,内部实现换成了 TimSort,其对象间比较的实现要求更加严格。
117.HashMap 和 Hashtable 有什么区别?
(1) HashMap允许key和value为null,而HashTable不允许。
(2) HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境, HashTable适合多线程 环境。
(3)在Java1.4中引入了LinkedHashMap , HashMap的一个子类,假如你想要遍历顺序,你很容易从 HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。
(4) HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration 进行遍历,它不支持fail-fast。
(5) HashTable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用 CocurrentHashMap。
7.如何决定使用 HashMap 还是 TreeMap?
对于在 Map 中插入、删除、定位一个元素这类操作, HashMap 是最好的选择,因为相对而言
HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择。
118.说一下 HashMap 的实现原理?
HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储, get(key)来获取。当传入 key 时,
HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出 的 hash 值相同时,我们称之为 hash 冲突, HashMap 的做法是用链表和红黑树存储相同 hash 值的
value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。
119.说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的, HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实 现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成, HashSet 不允许重复的值。
120.ArrayList 和 LinkedList 的区别是什么?
标签:面试题,Java,对象,引用,类型,属性,方法,大全 From: https://blog.csdn.net/weixin_44694333/article/details/140734190数据结构实现: ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问效率: ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据 存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作, LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时, 更推荐使用 LinkedList。