Java 面试题
1,两个对象的hashCode一样,则equals也一定为true吗?
不一定。两个对象的hashCode相等并不意味着它们的equals方法一定返回true。
在Java中,hashCode和equals是两个不同的方法,它们的实现可以独立于彼此。hashCode方法用于计算对象的哈希码值,而equals方法用于比较对象的内容是否相等。
根据hashCode的定义,如果两个对象的hashCode相等,那么它们被认为是"相等的"。这意味着它们在哈希表等数据结构中可能被视为相同的键。但是,hashCode相等并不保证对象的内容相等,因此equals方法的结果可能为false。
在实际编程中,为了保持一致性,如果两个对象的equals方法返回true,则它们的hashCode方法应该返回相同的值。这是因为在使用哈希表等数据结构时,如果两个对象被认为是相等的(equals返回true),则它们应该具有相同的哈希码,以确保它们可以正确地被存储和检索。
因此,当重写equals方法时,通常也需要重写hashCode方法,以确保hashCode和equals的一致性。但是,hashCode相等并不保证equals一定返回true。
2,&与&&和|和||的区别
注意事项:
位运算符&和|对整数类型的每个位执行操作,不会短路求值。即使第一个操作数的结果已经确定,第二个操作数也会被计算。
逻辑运算符&&和||对布尔类型进行操作,具有短路求值的特性。如果第一个操作数的结果已经确定,第二个操作数将不会被计算。
逻辑运算符&&和||的操作数可以是任何布尔表达式,而位运算符&和|的操作数必须是整数类型。
因为&&的判断,我们可以把错的几率多的放在第一个判断,这样可以节省时间,||也是一样的
3,java中的参数传递时,是传值呢?还是传引用呢?
对于基本类型来说,我们是传递的值,如果原来是
i = 5
,我们写了一个方法public static void changeData(int nn) { nn = 10; }
在通过这个方法之后,在主程序打印i的结果还是5;
对于对象来说,开始我们在主程序中
sb = new StringBuffer(“hello”)
,这个时候我们写一个方法,把sb传入public static void changeData(StringBuffer strBuf) { strBuf.append("World!"); }
接着在主程序中打印这个sb,我们会发现,变成了helloWord
但是
如果我们把上面的方法做一点变换
public static void changeData(StringBuffer strBuf) {// 0x11 stu strBuf = new StringBuffer("Hi ");// 0x12 Hi strBuf.append("World!"); } changeData("Stu")
我们重新在主程序中打印这个方法
我们会发现输出结果是Hello
这是为什么呢?
4,什么是 Java 的序列化,如何实现 Java 的序列化?
Java的序列化是指将对象转换为字节流的过程,以便在网络上传输或将对象持久化到磁盘中。序列化可以将对象的状态保存下来,以便在需要时重新创建对象。
要实现Java的序列化,需要满足以下条件:
类必须实现
java.io.Serializable
接口。这是一个标记接口,没有任何方法需要实现。所有非序列化的字段必须标记为
transient
关键字,以避免序列化。private String name; private transient int age;
在这里面,当我们把这个对象序列化之后,再反序列化,只能拿到name,无法拿到age
5,Java 中的反射是什么意思?
Java中的反射是指在运行时动态地获取类的信息(如类的属性、方法、构造函数等),并能够在运行时操作类的成员。通过反射,可以在运行时检查和修改类的结构、调用类的方法、创建对象等。
Java的反射机制提供了以下功能:
获取类的信息:可以获取类的名称、父类、接口、字段、方法、构造函数等信息。
创建对象:可以通过反射创建类的实例,即使在编译时无法确定具体的类。
调用方法:可以通过反射调用类的方法,包括公共方法、私有方法、静态方法等。
访问和修改字段:可以通过反射获取和修改类的字段的值,包括公共字段、私有字段等。
动态代理:可以使用反射实现动态代理,动态生成代理类并在运行时处理方法调用。
反射的核心类是
java.lang.reflect
包中的Class
类和java.lang.reflect
包中的其他类,如Field
、Method
、Constructor
等。反射在某些情况下非常有用,例如在框架、库和工具中,可以根据运行时的需求动态地加载和使用类。但是,由于反射涉及到运行时的动态操作,可能会导致性能下降和安全性问题,因此在使用反射时需要谨慎考虑。
6,反射的应用场景有哪些?优缺点呢?
反射在Java中有许多应用场景,包括但不限于以下几个方面:
动态加载类:通过反射可以在运行时动态地加载类,可以根据配置文件或用户输入的类名来加载相应的类。
创建对象:通过反射可以在运行时动态地创建对象,即使在编译时无法确定具体的类。
调用方法:通过反射可以在运行时动态地调用类的方法,包括公共方法、私有方法、静态方法等。
访问和修改字段:通过反射可以在运行时动态地获取和修改类的字段的值,包括公共字段、私有字段等。
动态代理:通过反射可以实现动态代理,动态生成代理类并在运行时处理方法调用。
框架和库:许多框架和库使用反射来实现插件机制、依赖注入、ORM(对象关系映射)等功能。
优点:
动态性:反射允许在运行时动态地获取和操作类的信息,使得代码更加灵活和可扩展。
适应性:反射可以处理未知类型的对象,使得代码可以处理各种不同的类和对象。
框架和库支持:许多框架和库使用反射来实现各种功能,如动态代理、依赖注入等。
缺点:
性能开销:反射操作通常比直接调用代码更慢,因为它需要在运行时进行额外的检查和解析。
安全性问题:反射可以绕过访问控制,访问和修改私有成员,可能导致安全性问题。
可读性和维护性:反射使代码更加复杂和难以理解,降低了代码的可读性和维护性。
因此,在使用反射时需要权衡其优缺点,并谨慎考虑是否真正需要使用反射来解决问题。在性能要求高、安全性要求严格或代码可读性和维护性重要的情况下,应该慎重使用反射。
7,实现动态代理的两种方式
基于接口的动态代理:这种方法要求被代理的类实现一个接口。使用
java.lang.reflect
包中的Proxy
类和InvocationHandler
接口来创建代理对象。具体步骤如下:
创建一个实现
InvocationHandler
接口的类,该类负责实现代理对象的方法调用逻辑,里面会重写invoke方法使用
Proxy
类的newProxyInstance()
方法创建代理对象,传入被代理对象的类加载器、被代理对象实现的接口数组和InvocationHandler
对象。使用代理对象调用方法,实际上会调用
InvocationHandler
接口的invoke()
方法。基于类的动态代理:这种方法不要求被代理的类实现接口,可以直接代理类本身。使用第三方库,如CGLIB,来实现基于类的动态代理。具体步骤如下:
引入CGLIB库的依赖。
创建一个类,作为代理类的父类。
创建一个实现
MethodInterceptor==
接口的类,该类负责实现代理对象的方法调用逻辑。使用CGLIB库的
Enhancer
类创建代理对象,设置父类和方法拦截器。使用代理对象调用方法,实际上会调用方法拦截器的
intercept()
方法。
8,String为什么要设计成不可变类?
String s1 = “hi”; s1 = “hello”;
这个不是可变的,在第二行,我们会先去常量池中找有没有hello,如果找到了,就把s1指向了hello,如果没有,那就创建一个来指向,所以这个String并没有发生改变。
为什么不可变?
String这个类是被final修饰的,不能被继承,不会被子类的方法覆盖
String内部通过
private final char[]
实现的,从而保证了引用的不可变和对外的不可变String内部良好的封装,也不会改变这个数组的值
为什么设计成不可变?
字符串池优化:不可变性允许字符串 共享和重用 ,节省内存,提高性能
线程安全性:不可变性,线程天然安全,不会出现多线程同时修改的错误,无需额外线程同步
缓存哈希值:不可变性可以让 哈希值被缓存 ,提高数据结构的性能
安全性与可靠性: 确保 实力状态不会被修改 ,使用于处理敏感信息等安全场景
方便共享和重用:可以 自由共享和重用 ,提高性能
TIPS
可以通过反射改变String中value的值,所以严格来说,不一定不可变
9,String、StringBuilder、StringBuffer 的区别?
可变性:String是不可变类,即一旦创建就不能被修改。而StringBuilder和StringBuffer是可变类,可以进行字符串的修改、拼接、替换等操作。
线程安全性:String是线程安全的,因为它是不可变类,多个线程可以同时访问和共享String对象。而StringBuilder是非线程安全的,多个线程同时访问和修改StringBuilder对象可能会导致数据不一致的问题。StringBuffer是线程安全的,它的方法都是使用synchronized关键字进行同步,保证了多线程环境下的安全性。
性能:由于String是不可变类,每次对String进行修改、拼接、替换等操作时,都会创建一个新的String对象,这样会频繁地进行内存分配和拷贝,影响性能。而StringBuilder和StringBuffer是可变类,它们在进行字符串操作时,不会创建新的对象,而是在原有对象上进行修改,避免了频繁的内存分配和拷贝,提高了性能。StringBuilder相对于StringBuffer在性能上更优,因为StringBuilder的方法没有使用synchronized关键字进行同步。
使用场景:由于String是不可变类,适合在多线程环境下使用,也适合作为方法参数和返回值。StringBuilder和StringBuffer适合在单线程环境下进行字符串的频繁修改、拼接、替换等操作,例如在循环中拼接字符串、构建大量字符串等。
总结:String是不可变类,线程安全,适合多线程环境和作为方法参数和返回值;StringBuilder是非线程安全的,性能较好,适合单线程环境下频繁修改字符串;StringBuffer是线程安全的,性能较差,适合多线程环境下频繁修改字符串。
10,String str = “i” 与 String str = new String(“i”) 一样吗?
是不一样的
String str = “i”
,会在常量池中寻找是否有i,如果有,那就会指向它,并不会创建新对象,如果没有,那就在常量池中新建对象
String str = new String(“i”)
,而这个首先是先把i放在了常量池中,之后的new String(“i”)
会创建一个新的String对象,值和常量池中的一样,但是是在堆中创建的
11,浅拷贝和深拷贝
浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存,基本数据类型会复制值
User user1 = new User("user1"); User user2 = user1;
深拷贝:复制并创建一个一模一样的对象,不共享内存空间,修改新对象,旧对象不变,所以可以重写clone方法,在克隆的时候,把所有的引用类型new一下就好
TIPS
要注意,在浅拷贝中,要尤为注意 String 类型,因为String是不可以变的,所以当浅拷贝之后,修改user2的值,并不会影响user1。
不建议使用clone方法来使用,可以使用,拷贝构造函数和拷贝工厂来拷贝一个对象,或者使用外部库来深拷贝
12,Overload、Override、Overwrite的区别
Overload(重载):
一个类中,参数列表或者返回值不一样。
Override(重写):
发生在父子类中,参数列表和返回值是不能改变的,只能重写方法体,子类的方法不能比父类的 访问性 更严格,子类抛出的异常不能比父类抛出的异常更多。
Overwrite(覆盖):
在文件操作中,覆盖是指将已有的文件替换为新的内容
经常发生在文件写入时,用新的内容覆盖原有的内容,使其被替代
覆盖可能会导致原文件的内容丢失,因此在覆盖操作的时候要小心
13,Exception和Error有什么区别?
是两个不同的类,都继承自Throwable类
异常
在程序执行过程中可能出现的可处理的异常情况,他一般由代码逻辑错误,外部条件变化等原因引起的,可以通过适当的处理来使得程序继续正常执行
受检异常:编译器要求必须在代码中显式的处理受检异常,否则代码无法通过编译,比如常见的io异常和sql异常
非受检异常:编译器对这种异常不强制要求执行,但是可以选择处理或者抛给上层处理,比如空指针或者数组下标越界异常
错误
指应用程序无法处理或者恢复的严重问题
通常表示JVM的错误状态或者系统级错误,比如内存溢出
通常意味着应用程序处于不可恢复的状态,所以一般不被捕获和处理
14,IO流分类
IO流根据功能和作用分为4种
字节流
以字节为单位读写
InputStream:字节输入流的抽象基类,是所有字节输入流的超类
OutputStream:字节输出流的抽象基类,是所有字节输出流的超类
实现类:FileInputStream,FileOutputStream,ByteArrayInputStream,ByteArrayOutputStream
字符流
以字符为单位读写
Reader:字符输入流的抽象基类,是所有字符输入流的超类
writer:字符输出流的抽象基类,是所有字符输出流的超类
实现类:FileReader,Filewriter,BufferReader,Bufferwriter
缓冲流
使用内部缓冲区,在读写时减少对磁盘的读写,提高效率
对象流
用于读写java对象的流,用于文件或者系列化到网络中
15,Hashtable和HashMap
初始容量不同,Hashtable 的初始长度是 11,之后每次扩充容量变为之前的 2n+1(n 为上一次的长度)而 HashMap 的初始长度为 16,之后每次扩充变为原来的两倍。
Hashtable 是线程安全,推荐使用 HashMap 代替 Hashtable;如果需要线程安全高并发的话,推荐使用 ConcurrentHashMap
代替 Hashtable。
ConcurrentHashMap:
数据结构: ConcurrentHashMap
的底层数据结构是一个分段的哈希表(Segmented Hash Table),它将整个哈希表分成多个段(Segment),每个段都是一个独立的哈希表。每个段都维护了一个独立的锁,不同的线程可以同时访问和修改不同的段,从而实现了并发访问的能力。
每个段内部的结构与HashMap
类似,使用数组和链表(或红黑树)的组合来存储键值对。每个段都有一个计数器,用于记录该段中的键值对数量。
底层原理: ConcurrentHashMap
的并发性能得益于分段锁的设计。当多个线程同时访问不同的段时,它们可以并发地进行读写操作,不会相互阻塞。这样可以提高并发性能,减少了锁的竞争。
当多个线程同时访问同一个段时,ConcurrentHashMap
会对该段进行细粒度的锁定,只有访问同一个段的线程会被阻塞,而其他段的访问不会受到影响。这样可以最大程度地减少锁的竞争,提高并发性能。
在ConcurrentHashMap
中,读操作不需要加锁,可以并发地进行。而写操作需要加锁,但是由于分段锁的设计,不同段的写操作可以并发进行,从而提高了写操作的并发性能。
需要注意的是,ConcurrentHashMap
虽然是线程安全的,但并不保证对于单个操作的原子性。例如,putIfAbsent()
方法并不是原子的,它由多个操作组成,可能会出现竞态条件。如果需要保证原子性,可以使用put()
方法结合AtomicReference
等原子类来实现。
总结起来,ConcurrentHashMap
是一个线程安全的哈希表实现,底层采用分段的哈希表结构。它通过分段锁的设计,实现了高效的并发访问和修改操作。在多线程环境下,ConcurrentHashMap
是一个高性能的并发容器。