Java基础与面试-每日必看(3)

前言

  每天几分钟,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("飞机呼啸着冲上云霄!");
    }
}

BirdAirplane 没有血缘关系(它们可不是一家的),但它们都能飞,所以它们都实现了 Flyable。接口不管你是鸟还是飞机,只要你能飞,就行!

我能干啥”和“我是啥”的区别

  1. 抽象类里可以有普通方法,接口里只能有抽象方法(JDK 8 之后支持 default 方法,但本质还是行为约束)
  2. 抽象类可以有各种类型的成员变量,而接口里的变量默认是 public static final(常量)
  3. 抽象类只能继承一个(单继承),而接口可以多个一起上(多实现)
class Superman implements Flyable, Runnable {
    @Override
    public void fly() {
        System.out.println("超人展翅高飞!");
    }

    @Override
    public void run() {
        System.out.println("超人用光速狂奔!");
    }
}

超人既能飞,又能跑,这在接口里一点压力都没有。如果用抽象类,就麻烦了,因为 Java 只支持单继承,Superman 不能同时继承 BirdCheetah(猎豹)。

什么时候用抽象类?什么时候用接口?

  • 你在意“身份”的时候,选抽象类
    • 🏎 例子CarVehicleDogAnimal,这些东西天生有个归属,适合用抽象类。
  • 你在意“能力”的时候,选接口
    • 🚀 例子Superman 既是 Person,但他还能 FlyableRunnable,这时候就得用接口了。
  • 抽象类 就像武侠小说里的门派,你要是个“少林弟子”,就得学少林功夫,继承一套完整的武功套路。
  • 接口 就像“江湖技能认证”,你可以拿到“轻功证书”、“内功证书”、“剑法证书”……想学几样技能自己选。

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:就像一个 带管理员的老式储物柜,每次存取都要排队(线程安全),但因为管得太严,效率可能会受影响。

主要区别

特性HashMapHashTable
线程安全❌ 不是线程安全的(要自己加锁)✅ 自带 synchronized,线程安全
是否允许 null✅ 允许 null 作为 key 和 value❌ 不能有 null 作为 key 或 value
效率🚀 高(无同步锁)🐢 低(有同步锁)
适用场景适合 单线程高并发但手动加锁适合 多线程但性能要求不高

底层实现:数组 + 链表(+ 红黑树)

无论是 HashMap 还是 HashTable,底层的核心结构都是 数组 + 链表,不过 HashMap 在 JDK8 之后又增加了 红黑树 来优化性能。

来看 HashMap 的数据存储过程:

  1. 计算 key 的 hash 值

    • 先计算 key 的 hashCode(),再经过 二次 hash 处理,避免哈希冲突过多。
    • 最终通过取模运算 hash % 数组长度,找到存储的位置(索引)。
  2. 存储元素

    • 如果这个位置是空的(没有哈希冲突),直接存入数组,变成 Node 节点。
    • 如果这个位置已经有数据(哈希冲突了),先检查:
      • key 是否相等equals() 比较),如果相等,就替换旧值
      • key 不相等,则插入 链表 末尾。
      • 如果链表长度达到 8 且数组长度超过 64,链表变成 红黑树 以提升查询效率。
  3. 删除和扩容

    • key 为 null 时,数据被存在 数组下标 0 的位置。
    • 扩容机制:当数据量超过 负载因子(默认 0.75)× 数组长度,就会 扩容为原来的 2 倍,然后重新计算哈希位置(rehash)。
    • 如果红黑树的元素个数少于 6,会从 红黑树退化为链表
// 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 是一个持续进阶的过程,建议大家在掌握基础知识的同时,多刷经典面试题、动手写代码,并结合实际项目进行实践。📚💻

如果你觉得这篇文章对你有所帮助,别忘了 点赞 👍、收藏 ⭐、关注 🔥!欢迎在评论区交流你的见解,我们一起进步!🚀🚀🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry-Walker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值