1、Java的String类型可以被继承吗?

Java中的String类被声明为final,因此不能被继承。以下是具体原因和影响:

final关键字的作用
final修饰的类禁止被其他类继承。String类的定义如下:

public final class String implements Serializable, Comparable<String>, CharSequence { ... }

设计目的

  1. 不可变性
    String的不可变性(如substringconcat等操作生成新对象)依赖final类保证。若允许继承,子类可能破坏不可变性的设计原则。

  2. 安全性
    String广泛用于JVM底层(如类加载机制)、哈希存储(如HashMap的键)。允许继承可能导致恶意子类篡改行为,引发安全漏洞。

  3. 性能优化
    JVM对String有特殊优化(如字符串常量池),假设String不可变是优化前提。继承可能干扰这些优化。

替代方案

若需扩展字符串功能,可通过以下方式实现:

  • 工具类:封装静态方法处理字符串(如StringUtils)。

  • 组合模式:持有String成员并提供扩展方法。

class CustomString {
    private String str;
    // 添加自定义方法
    public String reverse() { ... }
}

2、String、StringBuilder、StringBuffer的区别?

区别概述

String、StringBuilder和StringBuffer均为Java中用于处理字符串的类,主要区别在于可变性线程安全性性能

String(不可变字符串)

  • 不可变性:String对象一旦创建,其内容不可修改。任何修改操作(如拼接、替换)均会生成新对象。

  • 线程安全:因不可变性,天然线程安全。

  • 性能:频繁修改会导致大量对象创建,内存和性能开销较大。

  • 适用场景:字符串常量或无需修改的场景。

StringBuilder(可变字符串,非线程安全)

  • 可变性:直接修改内部字符数组,无需创建新对象。

  • 线程安全:非线程安全,适用于单线程环境。

  • 性能:比StringBuffer更高(无同步开销)。

  • 适用场景:单线程下频繁修改字符串。

StringBuffer(可变字符串,线程安全)

  • 可变性:与StringBuilder类似,支持原地修改。

  • 线程安全:通过synchronized方法保证线程安全。

  • 性能:因同步锁,性能略低于StringBuilder。

  • 适用场景:多线程下频繁修改字符串。

性能对比

  • 执行速度:StringBuilder > StringBuffer > String(频繁修改时)。

  • 内存占用:String因不可变性可能产生更多临时对象。

代码示例

// String(生成新对象)
String str = "Hello";
str += " World"; // 新对象

// StringBuilder(原地修改)
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 直接修改

// StringBuffer(线程安全修改)
StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World"); // 同步修改

选择建议

  • 无需修改且需线程安全:String
  • 单线程频繁修改:StringBuilder
  • 多线程频繁修改:StringBuffer

3、ArrayList、LinkedList的区别?

ArrayList基于动态数组实现,适合查询,查询复杂度为o(1);

LinkedList基于链表实现,适合插入、删除,查询复杂度为o(n);

4、讲一下HashMap?

HashMap 是 Java 集合框架中的一种数据结构,基于哈希表实现,用于存储键值对(Key-Value)。它允许键(Key)和值(Value)为 null,并且是非线程安全的。HashMap 的底层通过数组和链表(或红黑树)的组合实现高效的查找、插入和删除操作。

工作原理

当向 HashMap 中插入一个键值对时,HashMap 会通过键的 hashCode() 方法计算哈希值,再通过哈希函数(如取模运算)确定其在数组中的索引位置。如果多个键的哈希值冲突(即计算到相同的索引),HashMap 会使用链表或红黑树存储这些键值对。

  • 链表(JDK 1.7 及之前):冲突的键值对以链表形式存储,查找时需遍历链表。

  • 红黑树(JDK 1.8 及之后):当链表长度超过阈值(默认为 8)时,链表转换为红黑树,提高查找效率。

  • 时间复杂度

    • 理想情况下(无哈希冲突):O(1)

    • 最坏情况下(哈希冲突严重):O(log n)(JDK 1.8 后红黑树优化)。

  • 负载因子(Load Factor):默认为 0.75,表示当 HashMap 容量达到 75% 时触发扩容(数组大小翻倍)。

HashMap 是非线程安全的,多线程环境下可能导致数据不一致。若需线程安全,可使用:

  • ConcurrentHashMap:分段锁或 CAS 机制实现高效并发。

  • Collections.synchronizedMap(Map):通过同步包装器实现线程安全,但性能较低。

5、HashMap中链表为什么要转为红黑树?

性能优化

链表在查找时需要遍历,时间复杂度为O(n),而红黑树的查找时间复杂度为O(log n)。当链表长度较长时,转换为红黑树能显著提升查询效率。

防止哈希碰撞攻击

恶意构造大量哈希冲突的数据可能导致链表过长,使得HashMap退化为链表结构。红黑树的平衡性可以避免这种极端情况下的性能恶化。

阈值控制

Java 8中设定当链表长度超过8且数组长度大于等于64时,链表会自动转为红黑树。这个阈值经过测试能平衡空间和时间开销。

6、sleep和wait的区别

sleep属于thread类,不会释放锁,可以在任意位置调用,超时自动恢复或通过 interrupt() 强制唤醒

wait属于object类,会释放锁(必须配合 synchronized 使用),只能在同步块内调用,需要显式notify/notifyall唤醒

7、Synchonize和ReentrantLock的区别

实现机制

  • synchronized 是 Java 语言内置的关键字,基于 JVM 的监视器锁(Monitor)实现,无需手动释放锁。

  • ReentrantLock 是 Java 并发包(java.util.concurrent.locks)提供的类,基于 API 实现,需要显式调用 lock()unlock() 方法管理锁。

锁的获取与释放

  • synchronized 在进入同步代码块时自动获取锁,退出时自动释放,包括异常退出。

  • ReentrantLock 必须显式调用 unlock() 释放锁,通常结合 try-finally 块确保锁释放,避免死锁。

功能特性

  • ReentrantLock 提供更灵活的锁控制:

    • 可以公平锁:通过构造函数指定公平性(new ReentrantLock(true)),减少线程饥饿。

    • 可中断lockInterruptibly() 允许在等待锁时响应中断。

    • 超时机制tryLock(long timeout, TimeUnit unit) 可设置获取锁的超时时间。

  • synchronized 不支持上述功能,但代码更简洁。

性能差异

  • 在 Java 6 及之后版本,synchronized 经过优化(如偏向锁、轻量级锁),性能与 ReentrantLock 接近。

  • 高竞争场景下,ReentrantLock 的吞吐量可能更高,但需权衡代码复杂性。

使用场景

  • synchronized 适合简单的同步需求,代码更易维护。

  • ReentrantLock 适合需要高级功能(如公平性、超时)的复杂场景。

示例代码对比

synchronized 示例

public synchronized void method() {
    // 同步代码块
}

ReentrantLock 示例

private final ReentrantLock lock = new ReentrantLock();

public void method() {
    lock.lock();
    try {
        // 同步代码块
    } finally {
        lock.unlock();
    }
}

具体可参看下面博客

[java 锁 02 - synchronized vs ReentrantLock ] - 十三山入秋 - 博客园

8、进程之间如何通信

管道(Pipe)
管道是半双工的,数据只能单向流动。通常用于父子进程通信。创建管道使用pipe()系统调用,一个进程写入数据,另一个进程读取。

命名管道(FIFO)
命名管道允许无亲缘关系的进程通信。通过mkfifo()创建,像普通文件一样操作,但数据遵循先进先出原则。

消息队列
消息队列是消息的链表,存储在内核中。进程通过msgget()msgsnd()msgrcv()发送和接收消息。克服了管道只能承载无格式字节流的缺点。

共享内存
共享内存是最快的IPC方式,多个进程映射同一块内存区域。通过shmget()创建,shmat()附加到进程地址空间。需配合信号量等机制解决同步问题。

信号量
信号量用于进程间同步,控制对共享资源的访问。通过semget()semop()操作,支持PV原语(等待和信号操作)。

套接字(Socket)
套接字支持不同主机或同一主机的进程通信。分为流式套接字(TCP)和数据报套接字(UDP),提供全双工通信能力。

信号(Signal)
信号用于通知进程异步事件。通过kill()发送信号,signal()sigaction()处理信号。适用于少量数据的简单通知。

9、get请求和post请求的区别

HTTP请求方法的本质差异

GET请求用于从服务器获取数据,参数直接附加在URL后,可见且长度受限(约2048字符)。POST请求用于向服务器提交数据,参数包含在请求体中,适合传输敏感或大量数据(如文件上传)。

安全性对比

GET请求参数暴露在URL中,可能被浏览器历史记录或服务器日志保存,不适合传递密码等敏感信息。POST请求数据在请求体内,相对更安全,但仍需配合HTTPS加密确保传输安全。

数据长度与编码限制

GET请求受URL长度限制,不适合传输大量数据。POST请求无严格长度限制,支持多种编码类型(如multipart/form-data),可上传二进制文件。

幂等性与缓存特性

GET请求是幂等的(多次执行结果相同),可被缓存或收藏为书签。POST请求非幂等(如重复提交订单可能产生多次交易),通常不被缓存。

10、url请求的全过程

1、DNS解析:浏览器检查本地缓存是否存在该域名对应的IP地址。若不存在,向DNS服务器发起递归查询,最终获取目标服务器的IP地址。

2、建立TCP连接:通过三次握手与服务器建立TCP连接。客户端发送SYN包,服务器回应SYN-ACK包,客户端再发送ACK包确认连接。

3、发送HTTP请求:建立连接后,浏览器构建HTTP请求报文,包含请求方法、请求头、请求体等信息,通过TCP连接发送给服务器。

4、服务器处理请求:服务器接收请求后,根据路径和参数处理请求,可能涉及数据库查询、业务逻辑处理等操作,生成响应数据。

5、返回HTTP响应:服务器将响应数据封装为HTTP响应报文,包含状态码、响应头、响应体等信息,通过TCP连接返回给客户端。

6、浏览器解析渲染:客户端接收响应后,解析HTML文档,构建DOM树和CSSOM树,合并为渲染树,进行布局和绘制,最终呈现页面内容。

7、关闭TCP连接:完成数据传输后,通过四次挥手断开TCP连接。客户端发送FIN包,服务器回应ACK包,服务器发送FIN包,客户端回应ACK包确认断开。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐