前言
每天几分钟,Java不再难。关注我,让我们一起在Java的世界里畅游,成为面试场上的常胜将军!
抽象类 vs. 接口:你是“本质派”还是“行为派”?
在 Java 这个世界里,有两种“老司机”:抽象类(Abstract Class)和接口(Interface)。这俩谁更厉害?其实不是谁牛逼的问题,而是你要看需求,选对工具才是王道。
你是谁 vs. 你能干啥
-
抽象类就像“⽼⼤哥”,它关心的是——你是谁(is-a)。比如:
abstract class Car {
String brand;
void startEngine() {
System.out.println(brand + " 的发动机启动了!");
}
abstract void drive(); // 这个方法,子类必须实现
}
class BMW extends Car {
BMW() { this.brand = "BMW"; }
@Override
void drive() {
System.out.println("BMW 风驰电掣!");
}
}
-
这里
BMW
继承了Car
,它天生就是车,Car
定义了一些车的基本行为,比如startEngine()
(所有车都得点火),但“怎么开”这个细节得留给具体的车自己去搞定。 -
接口则像“老司机驾照”,它关心的是——你能干啥(like-a)。比如:
interface Flyable {
void fly(); // 只规定你要能飞,怎么飞你自己决定
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("鸟儿拍着翅膀飞上天!");
}
}
class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("飞机呼啸着冲上云霄!");
}
}
Bird
和 Airplane
没有血缘关系(它们可不是一家的),但它们都能飞,所以它们都实现了 Flyable
。接口不管你是鸟还是飞机,只要你能飞,就行!
我能干啥”和“我是啥”的区别
- 抽象类里可以有普通方法,接口里只能有抽象方法(JDK 8 之后支持
default
方法,但本质还是行为约束) - 抽象类可以有各种类型的成员变量,而接口里的变量默认是
public static final
(常量) - 抽象类只能继承一个(单继承),而接口可以多个一起上(多实现)
class Superman implements Flyable, Runnable {
@Override
public void fly() {
System.out.println("超人展翅高飞!");
}
@Override
public void run() {
System.out.println("超人用光速狂奔!");
}
}
超人既能飞,又能跑,这在接口里一点压力都没有。如果用抽象类,就麻烦了,因为 Java 只支持单继承,Superman
不能同时继承 Bird
和 Cheetah
(猎豹)。
什么时候用抽象类?什么时候用接口?
- 你在意“身份”的时候,选抽象类
- 🏎 例子:
Car
是Vehicle
,Dog
是Animal
,这些东西天生有个归属,适合用抽象类。
- 🏎 例子:
- 你在意“能力”的时候,选接口
- 🚀 例子:
Superman
既是Person
,但他还能Flyable
和Runnable
,这时候就得用接口了。
- 🚀 例子:
- 抽象类 就像武侠小说里的门派,你要是个“少林弟子”,就得学少林功夫,继承一套完整的武功套路。
- 接口 就像“江湖技能认证”,你可以拿到“轻功证书”、“内功证书”、“剑法证书”……想学几样技能自己选。
List和Set的区别
想象一下,List 就像一个按顺序排队的电影院队伍,每个人(元素)都有自己的固定位置,进来的顺序就是他们的座位顺序。而且,如果你想,你可以让同一个人买好几张票,占据多个位置(允许重复)。甚至整个队伍里可以有好几个“隐形人”(多个 null
也没问题)。如果你想找某个人,你可以直接说:“给我看看第 3 个人是谁!”(get(int index)
)
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add("Alice"); // 重复没问题
list.add(null);
list.add(null); // 多个 null 也没问题
System.out.println(list.get(2)); // 输出 "Alice"
而 Set 就像是一个严格的俱乐部,保安特别较真——不准重复! 你进去了,就不能有第二个你。甚至有时候,连“隐形人”(null
)都只能有一个,不能有多个。更让人崩溃的是,这里的客人们不按顺序站队,保安喜欢随心所欲地安排座位,你不能直接点名要第几个,你只能靠“扫视全场”(用 Iterator
遍历)。
Set<String> set = new HashSet<>();
set.add("Alice");
set.add("Bob");
set.add("Alice"); // 这个 Alice 直接被拒
set.add(null);
set.add(null); // 只能有一个 null
System.out.println(set); // 顺序可能不是 ["Alice", "Bob", null],而是随机的
所以,总结一下:
- List 是一群排好队的观众,可以有重名的,还有人愿意站在不同位置(可重复)。
- Set 是个严谨的俱乐部,没人能分身术,大家还不能随意站队(不重复,顺序不固定)
ArrayList vs LinkedList:谁是你的代码最佳搭档?
想象一下,你有两种存放人名的方式:
- ArrayList:就像一排整整齐齐的储物柜(数组),每个柜子都有编号,你可以立刻找到你要的东西!
- LinkedList:就像一串连锁的便利店(链表),每家店都有指路牌,告诉你下一个店在哪儿,你要找某个东西,可能得一家家问过去。
所以,它们的区别主要是 底层的数据结构不同:
- ArrayList:基于数组实现,查询快,增删慢。
- LinkedList:基于链表实现,查询慢,增删快。
谁更适合你的场景?
-
如果你 经常查找(比如排行榜、通讯录)——ArrayList 更适合!
因为数组支持 随机访问,找到第n
个元素的时间复杂度是O(1)
,直接去编号对应的柜子里取就行。
List<String> arrayList = new ArrayList<>();
arrayList.add("Alice");
arrayList.add("Bob");
arrayList.add("Charlie");
System.out.println(arrayList.get(1)); // Bob
如果你 经常增删(比如订单队列、任务调度)——LinkedList 更合适!
因为链表在中间插入或删除的时间复杂度是 O(1)
,就像在连锁店中加入一家新店,不用挪动所有的店铺,只需要修改指路牌。
List<String> linkedList = new LinkedList<>();
linkedList.add("Alice");
linkedList.add("Charlie");
linkedList.add(1, "Bob"); // 在索引 1 处插入 Bob
System.out.println(linkedList); // [Alice, Bob, Charlie]
谁更“多才多艺”?
LinkedList 额外实现了 Deque
接口,这意味着它不仅能当列表用,还能当 队列 或 双端队列(Deque) 来用,比如模拟 排队买奶茶:
Deque<String> queue = new LinkedList<>();
queue.offer("Alice");
queue.offer("Bob");
queue.offer("Charlie");
System.out.println(queue.poll()); // Alice,先来先喝!
小结
特点 | ArrayList(数组派) | LinkedList(链表派) |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
查询效率 | 高 (O(1) ) | 低 (O(n) ) |
插入删除 | 慢 (O(n) ) | 快 (O(1) ) |
适合场景 | 读多写少 | 频繁增删 |
特殊能力 | 仅是 List | 还能当队列用! |
HashMap vs HashTable:到底谁更强?
想象一下,你有两个储物柜可以存放数据:
- HashMap:就像一个 自助式储物柜,谁都能来存东西,随取随放,但没有专人管理,容易被抢占(线程不安全)。
- HashTable:就像一个 带管理员的老式储物柜,每次存取都要排队(线程安全),但因为管得太严,效率可能会受影响。
主要区别
特性 | HashMap | HashTable |
---|---|---|
线程安全 | ❌ 不是线程安全的(要自己加锁) | ✅ 自带 synchronized ,线程安全 |
是否允许 null | ✅ 允许 null 作为 key 和 value | ❌ 不能有 null 作为 key 或 value |
效率 | 🚀 高(无同步锁) | 🐢 低(有同步锁) |
适用场景 | 适合 单线程 或 高并发但手动加锁 | 适合 多线程但性能要求不高 |
底层实现:数组 + 链表(+ 红黑树)
无论是 HashMap
还是 HashTable
,底层的核心结构都是 数组 + 链表,不过 HashMap
在 JDK8 之后又增加了 红黑树 来优化性能。
来看 HashMap
的数据存储过程:
-
计算 key 的 hash 值
- 先计算 key 的
hashCode()
,再经过 二次 hash 处理,避免哈希冲突过多。 - 最终通过取模运算
hash % 数组长度
,找到存储的位置(索引)。
- 先计算 key 的
-
存储元素
- 如果这个位置是空的(没有哈希冲突),直接存入数组,变成
Node
节点。 - 如果这个位置已经有数据(哈希冲突了),先检查:
- key 是否相等(
equals()
比较),如果相等,就替换旧值。 - key 不相等,则插入 链表 末尾。
- 如果链表长度达到 8 且数组长度超过 64,链表变成 红黑树 以提升查询效率。
- key 是否相等(
- 如果这个位置是空的(没有哈希冲突),直接存入数组,变成
-
删除和扩容
- key 为
null
时,数据被存在 数组下标 0 的位置。 - 扩容机制:当数据量超过 负载因子(默认 0.75)× 数组长度,就会 扩容为原来的 2 倍,然后重新计算哈希位置(rehash)。
- 如果红黑树的元素个数少于 6,会从 红黑树退化为链表。
- key 为
// HashMap 允许 null key 和 null value
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "value1"); // ✅ 可以存
hashMap.put("key1", null); // ✅ 可以存
// HashTable 不能存 null
Map<String, String> hashTable = new Hashtable<>();
hashTable.put(null, "value1"); // ❌ 报错:NullPointerException
hashTable.put("key1", null); // ❌ 报错:NullPointerException
总结
- 单线程用 HashMap,性能更高!
- 多线程可以用 HashTable,但更推荐
ConcurrentHashMap
(更优的线程安全方案)! - 链表长度超 8,数组长度超 64,链表变红黑树,减少查找时间。
- 数据量大了,HashMap 会自动扩容,别手动调容量太小,不然频繁扩容很耗性能!
结尾总结
以上就是本篇博客的全部内容,希望这篇文章能帮助大家在 Java 学习和求职面试中更加自信!💡
学习 Java 是一个持续进阶的过程,建议大家在掌握基础知识的同时,多刷经典面试题、动手写代码,并结合实际项目进行实践。📚💻
如果你觉得这篇文章对你有所帮助,别忘了 点赞 👍、收藏 ⭐、关注 🔥!欢迎在评论区交流你的见解,我们一起进步!🚀🚀🚀