集合相关
一、HashMap和HashTable的区别
HashMap 和 HashTable 都是 Java 中用于存储键值对的数据结构,但它们在一些方面有明显的区别。以下是它们的主要区别:
-
线程安全性:
HashMap:HashMap是非线程安全的,这意味着在多线程环境下使用时,需要额外的同步措施来保证线程安全。HashTable:HashTable是线程安全的,它的方法都被同步化了,因此在多线程环境下使用时,不需要额外的同步措施。然而,这也导致了在并发环境中性能可能不如HashMap。
-
继承和接口:
HashMap:继承自AbstractMap类,实现了Map接口。HashTable:继承自Dictionary类(已经过时),实现了Map接口。
-
空值(null值) :
HashMap:允许键和值都为null,可以存储一个键为null的条目,并且多个条目的值可以为null。HashTable:不允许键和值为null,任何试图存储null键或值的操作都会抛出异常。
-
性能和扩容:
HashMap:在进行数据扩容时,使用的是扩容机制,相对而言扩容时比较灵活,性能较好。HashTable:在进行数据扩容时,使用的是重新哈希(Rehashing)机制,性能相对较差。
-
迭代器:
HashMap:提供了fail-fast迭代器,如果在迭代过程中修改了HashMap的结构(增加、删除元素),会抛出ConcurrentModificationException异常。HashTable:不提供fail-fast迭代器,因为其方法都是同步化的,所以在迭代过程中不会出现并发修改的情况。
总的来说,如果你在单线程环境中工作,且不需要考虑键或值为 null 的情况,那么使用 HashMap 是合适的。如果需要在多线程环境中使用,或者需要支持键或值为 null,可以考虑使用 HashTable,但要注意性能方面的权衡。
Hashtable 是线程安全,推荐使用 HashMap 代替 Hashtable;如果需要线程安全高并发的话,推荐使用 ConcurrentHashMap 代替 Hashtable。
二、HashMap 与 ConcurrentHashMap 的异同
- 都是 key-value 形式的存储数据;
- HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;
- HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快;
- HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 * 数组大小的方式进行扩容;
- ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry, Segment 数组大小默认是 16,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized来保证并发安全进行实现。
三、HashMap 的长度为什么是 2 的 N 次方呢?
为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。
我们首先可能会想到 % 取模的操作来实现。
下面是回答的重点哟:
取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash &(length - 1) 的前提是 length 是 2 的 n 次方)。并且,采用二进制位操作 & ,相对于 % 能够提高运算效率。
假设哈希表的长度设置为 2 的 N 次方,如 16,
6 % 15 = 6
10 % 15 = 10
22 % 15 = 6
14 % 15 = 14
那么相应的哈希函数和位置计算将会变化:
6 & 15 = 6
10 & 15 = 10
22 & 15 = 6
14 & 15 = 14
这就是为什么 HashMap 的长度需要 2 的 N 次方了。
四、红黑树有哪几个特征?
紧接上个问题,面试官很有可能会问红黑树,下面把红黑树的几个特征列出来 红黑树是一棵二叉树, 有五大特征:
特征一: 节点要么是红色,要么是黑色(红黑树名字由来)。
特征二: 根节点是黑色的
特征三: 每个叶节点(nil或空节点)是黑色的。
特征四: 每个红色节点的两个子节点都是黑色的(相连的两个节点不能都是红色的)。 这保证了从任意节点到其子孙叶子节点的路径上不能有两个连续的红色节点,防止过深的路径。
特征五: 。从任意节点出发,到达其每个叶子节点的路径中,黑色节点的数量必须相同。这保证了树的黑色平衡,防止一侧路径过长。
面向对象相关
一、Java创建对象有几种方式?
在 Java 中,你可以使用多种方式来创建对象,具体取决于你的需求和代码结构。以下是常见的几种创建对象的方式:
-
使用
new关键字: 最常见的方式是使用new关键字创建对象。例如:MyClass obj = new MyClass(); -
通过反射: Java 反射机制允许你在运行时获取类的信息,并通过类的构造函数创建对象。例如:
javaCopy code Class<?> clazz = MyClass.class; MyClass obj = (MyClass) clazz.getDeclaredConstructor().newInstance(); -
通过反序列化: 通过反序列化可以将对象从字节流重新还原为内存中的对象。例如:
FileInputStream fileIn = new FileInputStream("object.ser"); ObjectInputStream in = new ObjectInputStream(fileIn); MyClass obj = (MyClass) in.readObject(); -
使用静态工厂方法: 在类中提供一个静态方法来创建对象,这样可以隐藏构造函数,提供更多的控制和灵活性。例如:
public class MyClass { private MyClass() { // 私有构造函数 } public static MyClass createInstance() { return new MyClass(); } } // 使用 MyClass obj = MyClass.createInstance();
静态工厂方法适用于简单的对象创建场景,提供一种简单的方式创建不同类型的对象。 5. 使用工厂模式: 工厂模式是一种设计模式,通过定义一个工厂类来封装对象的创建逻辑。例如:
```
interface MyFactory {
MyClass createInstance();
}
class DefaultFactory implements MyFactory {
public MyClass createInstance() {
return new MyClass();
}
}
// 使用
MyFactory factory = new DefaultFactory();
MyClass obj = factory.createInstance();
```
而工厂模式适用于需要更复杂的对象创建逻辑,需要将对象创建过程抽象出来,提供更高的灵活性和可维护性。
6. 使用单例模式: 单例模式用于确保类只有一个实例,并提供一个全局访问点。典型的单例模式会在内部创建一个实例并提供静态方法返回该实例。
1.1 单例模式与静态工厂方法的区别
当涉及到单例模式和静态工厂方法时,让我们通过具体的示例来理解它们的应用。
单例模式示例:
假设你有一个配置管理器类,用于管理应用程序的配置信息。你希望确保整个应用程序只有一个配置管理器实例,以便在不同部分共享同一配置。
public class ConfigurationManager {
private static ConfigurationManager instance = new ConfigurationManager();
private Map<String, String> configMap = new HashMap<>();
private ConfigurationManager() {
// 私有构造函数,防止外部实例化
}
public static ConfigurationManager getInstance() {
return instance;
}
public void setConfig(String key, String value) {
configMap.put(key, value);
}
public String getConfig(String key) {
return configMap.get(key);
}
}
在这个例子中,ConfigurationManager 类使用饿汉式单例模式来确保只有一个实例。通过 getInstance() 方法获取实例,然后可以调用方法设置和获取配置信息。
静态工厂方法示例:
假设你正在开发一个图形库,需要创建不同类型的图形对象。你可以使用静态工厂方法来创建这些对象。
public class ShapeFactory {
public static Shape createShape(String type) {
if ("circle".equalsIgnoreCase(type)) {
return new Circle();
} else if ("rectangle".equalsIgnoreCase(type)) {
return new Rectangle();
} else if ("triangle".equalsIgnoreCase(type)) {
return new Triangle();
}
throw new IllegalArgumentException("Unsupported shape type: " + type);
}
}
在这个例子中,ShapeFactory 类提供了一个静态工厂方法 createShape(),根据传入的类型字符串创建不同类型的图形对象。这样,客户端可以通过调用工厂方法来获取所需的图形对象,而不需要直接实例化对象。
总结起来,单例模式适用于需要全局唯一实例的情况,例如配置管理器;静态工厂方法适用于需要更大灵活性、根据参数创建不同类型实例的情况,例如图形库中的图形对象创建。
1.2 单例模式:懒汉式、饿汉式
你可以使用单例模式来确保一个类只有一个实例,并提供全局访问点来获取该实例。下面是一个使用单例模式实现配置管理器类的示例:
懒汉式
public class ConfigurationManager {
private static ConfigurationManager instance;
private Map<String, String> configMap = new HashMap<>();
// 私有构造函数,防止外部实例化
private ConfigurationManager() {
// 初始化配置信息
configMap.put("key1", "value1");
configMap.put("key2", "value2");
configMap.put("key3", "value3");
}
// 获取单例实例
public static ConfigurationManager getInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = new ConfigurationManager();
}
}
}
return instance;
}
public String getConfig(String key) {
return configMap.get(key);
}
}
在这个示例中,ConfigurationManager 类使用了懒汉式单例模式,通过私有的构造函数防止外部实例化,同时提供了一个静态的 getInstance() 方法来获取单例实例。在 getInstance() 方法内部,使用双重检查锁定(Double-Checked Locking)来确保在多线程环境下也能正确地获取单例实例。
你可以在应用程序的不同部分通过调用 ConfigurationManager.getInstance() 来获取同一个配置管理器实例,从而实现在整个应用程序中共享同一配置信息。
饿汉式
当使用饿汉式模式实现单例时,实例在类加载时就被创建,无需延迟加载。以下是使用饿汉式模式实现配置管理器类的示例代码:
javaCopy code
public class ConfigurationManager {
private static final ConfigurationManager instance = new ConfigurationManager();
private Map<String, String> configMap = new HashMap<>();
// 私有构造函数,防止外部实例化
private ConfigurationManager() {
// 初始化配置信息
configMap.put("key1", "value1");
configMap.put("key2", "value2");
configMap.put("key3", "value3");
}
// 获取单例实例
public static ConfigurationManager getInstance() {
return instance;
}
public String getConfig(String key) {
return configMap.get(key);
}
}
在这个示例中,ConfigurationManager 类的静态成员 instance 在类加载时被初始化,并且是 final 类型,确保了只有一个实例被创建。因此,无论何时调用 ConfigurationManager.getInstance() 都会获得同一个配置管理器实例。
饿汉式模式适用于在程序运行时就需要创建实例并且创建实例的开销较小的情况。但需要注意,在某些情况下,如果单例对象的初始化需要较多资源,可能会导致应用程序启动变慢。
二、深拷贝与浅拷贝
假设有一个包含引用类型字段的类 Person:
javaCopy code
class Person {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
如果使用浅拷贝复制一个 Person 对象:
javaCopy code
Person original = new Person("Alice", new Address("New York"));
Person shallowCopy = new Person(original.name, original.address);
在这种情况下,shallowCopy 的 address 字段与 original 的 address 字段指向同一个 Address 对象,修改 shallowCopy.address.city 会影响到 original.address.city。
如果使用深拷贝复制一个 Person 对象:
javaCopy code
Person original = new Person("Alice", new Address("New York"));
Person deepCopy = new Person(original.name, new Address(original.address.city));
在这种情况下,deepCopy 的 address 字段与 original 的 address 字段指向不同的 Address 对象,两者之间互不影响。
总之,浅拷贝复制的是引用,深拷贝复制的是对象本身。选择哪种方式取决于你对于复制后对象间关系的期望。
关键字相关
一、final有哪些用法?
在 Java 中,final 是一个关键字,它可以应用于不同的上下文,用于表示不可更改或不可继承的特性。以下是 final 关键字在不同情况下的几种用法:
-
final 修饰变量:
- 当
final修饰一个变量时,表示该变量的值一旦赋值后就不可再修改。这适用于基本数据类型和引用类型。 - 对于基本数据类型,一旦赋值后,变量的值不能再改变。
- 对于引用类型,一旦引用指向一个对象,就不能再指向其他对象,但对象本身的内容是可以修改的。
final int num = 10; // 不可修改的整数 final String name = "Alice"; // 不可修改的字符串 final List<String> names = new ArrayList<>(); // 引用不可变,但对象内容可变 - 当
-
final 修饰方法:
- 当
final修饰一个方法时,表示该方法不能被子类重写。子类不能对final方法进行覆盖或修改。
class Parent { final void printMessage() { System.out.println("Hello from Parent"); } } class Child extends Parent { // 尝试覆盖父类的 final 方法会导致编译错误 } - 当
-
final 修饰类:
- 当
final修饰一个类时,表示该类不能被继承。其他类不能扩展(继承)一个被标记为final的类。
final class FinalClass { // 不能被其他类继承 } // 尝试继承 final 类会导致编译错误 - 当
-
final 修饰实例变量:
- 当
final修饰实例变量(成员变量)时,表示每个实例对象都有一个独立的副本,并且变量在初始化后不能再被修改。
class MyClass { final int value; // 实例变量,每个实例都有自己的 value 副本 MyClass(int value) { this.value = value; } } - 当
-
final 修饰形参:
- 在方法声明中,将形参声明为
final表示该形参在方法体内不能被修改。
void processValue(final int x) { // x 不能在方法内部被修改 } - 在方法声明中,将形参声明为
总之,final 关键字用于表示不可变性、不可修改性、不可继承性,可以应用于变量、方法、类以及形参等多种上下文中。
二、static都有哪些用法?
在 Java 中,static 是一个关键字,用于修饰类的成员(字段、方法、块)以及内部类。static 的主要作用是表示成员是属于类而不是对象的,因此它在类加载时就被初始化,并且可以通过类名直接访问。以下是 static 关键字在不同情况下的几种用法:
-
静态变量(类变量) :
- 用于声明属于类而不是实例的变量,所有该类的实例共享同一个静态变量。
- 静态变量在类加载时初始化,只会被初始化一次。
javaCopy code class MyClass { static int count; // 静态变量,属于类而不是实例 } -
静态方法:
- 用于声明属于类而不是实例的方法,可以通过类名直接调用。
- 静态方法不能访问实例变量,因为它们不依赖于对象的状态。
javaCopy code class MathUtils { static int add(int a, int b) { return a + b; } } -
静态代码块:
- 用于在类加载时执行一些初始化操作,类似于构造函数,但在构造函数调用前执行。
- 静态代码块在类加载时只执行一次。
javaCopy code class InitClass { static { // 在类加载时执行的静态代码块 } } -
静态内部类:
- 用于声明一个嵌套在类中的静态内部类,与外部类的实例无关。
- 静态内部类可以通过类名直接访问,而不需要外部类的实例。
javaCopy code class Outer { static class Inner { // 静态内部类 } } -
静态导入:
- 用于在不使用类名限定的情况下,直接访问类中的静态成员。
javaCopy code import static mypackage.MyClass.myStaticMethod; // 静态导入静态方法 public class Main { public static void main(String[] args) { myStaticMethod(); // 可以直接调用静态方法,不需要类名限定 } }
总之,static 关键字主要用于将成员标记为属于类而不是实例,以及在类加载时进行初始化。它在许多场景下有用,如声明常量、工具方法、静态内部类等。然而,过度使用静态成员可能会导致耦合性增加,因此应根据具体需求谨慎使用。
IO相关
一、说说Java 中 IO 流
Java 中 IO 流分为几种? 按照流的流向分,可以分为输入流和输出流; 按照操作单元划分,可以划分为字节流和字符流; 按照流的角色划分为节点流和处理流。
Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:
按操作对象分类结构图:
二、Java IO与 NIO的区别
Java中的I/O(Input/Output)和NIO(New I/O)是处理输入和输出的两种不同的方式,它们在处理方式、性能和用途上有一些区别。以下是Java I/O和NIO之间的主要区别:
Java I/O:
-
面向流(Stream-Oriented) :
- Java I/O 是基于流的模型,它以字节流和字符流的方式处理数据。每次从输入源读取或向输出源写入一个字节或字符。
-
阻塞(Blocking) :
- Java I/O 是阻塞式的,即当线程从输入流或输出流读取或写入数据时,如果数据还没有准备好,线程将被阻塞。
-
没有缓冲区:
- Java I/O 操作通常没有内置的缓冲区,每次读写操作都直接与底层输入/输出源交互。
-
适用于较低的并发量:
- Java I/O 适用于较低的并发量,每个连接都需要一个单独的线程来处理,因此在大量并发连接时可能会导致资源耗尽。
Java NIO:
-
面向缓冲区(Buffer-Oriented) :
- Java NIO 是基于缓冲区的模型,数据被读取到缓冲区中,然后从缓冲区写入或读取数据。
-
非阻塞(Non-blocking) :
- Java NIO 是非阻塞的,线程在等待I/O操作完成时可以继续执行其他任务,而不必等待数据准备好。
-
有缓冲区:
- Java NIO 提供了缓冲区的支持,允许数据在内存中进行临时存储,减少了与底层资源的频繁交互。
-
适用于高并发:
- Java NIO 适用于高并发情况,通过使用选择器(Selector)和通道(Channel),一个线程可以处理多个连接。
-
提供了多路复用器:
- Java NIO 提供了 Selector,它允许一个线程监视多个通道的事件,实现了多路复用的功能,可以在一个线程中管理多个连接。
总之,Java I/O适用于较低的并发需求,适合简单的I/O操作。Java NIO适用于高并发、大量连接的情况,提供了更灵活的非阻塞I/O处理方式,以及基于缓冲区的数据处理方式。在选择使用哪种模型时,需要根据具体的应用需求和性能要求来进行权衡。
类型相关
一、3*0.1 == 0.3返回值是什么
在 Java 中,浮点数的精度有限,可能会导致某些计算结果不太准确。因此,当你执行 3 * 0.1 == 0.3 这样的比较时,可能会得到 false 的结果。这是因为浮点数的二进制表示可能无法完全准确地表示十进制的 0.1,从而导致计算误差。
在实际运行中,3 * 0.1 的结果可能会是一个非常接近 0.3 的值,但不一定精确等于 0.3。因此,比较的结果会返回 false。
为了进行浮点数的比较,最好使用一个误差范围来判断它们是否足够接近,而不是直接使用 == 运算符。例如:
javaCopy code
double result = 3 * 0.1;
double target = 0.3;
double epsilon = 1e-10; // 一个很小的误差范围
boolean isEqual = Math.abs(result - target) < epsilon;
System.out.println(isEqual); // 可能会输出 true
这样可以避免浮点数计算误差带来的比较问题。