美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

本文源于一次面试经历,讨论了Java中字符的String.length()返回值为2的原因,涉及到Unicode、UTF-16编码以及代码点和代码单元的概念。解释了字符“U+1D11E”的UTF-16编码过程,并探讨了UTF-32和UTF-8的代码单元。强调了技术人员不断学习的重要性。

public class testT {
    public static void main(String [] args){
        String A = "hi你是乔戈里";
        System.out.println(A.length());
    }
}

以上结果输出为7。

小萌边说边在IDEA中的win环境下选中String.length()函数,使用ctrl+B快捷键进入到String.length()的定义。

 /**
     * Returns the length of this string.
     * The length is equal to the number of <a href="Character.html#unicode">Unicode
     * code units</a> in the string.
     *
     * @return  the length of the sequence of characters represented by this
     *          object.
     */
    public int length() {
        return value.length;
    }

接着使用google翻译对这段英文进行了翻译,得到了大体意思:返回字符串的长度,这一长度等于字符串中的 Unicode 代码单元的数目。

Java中 有内码和外码这一区分简单来说

  • 内码:char或String在内存里使用的编码方式。
  • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)

而java内码:unicode(utf-16)中使用的是utf-16.所以上面的那句话再进一步解释就是:返回字符串的长度,这一长度等于字符串中的UTF-16的代码单元的数目。

[代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元] “代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元”)。UTF-X 中的数字 X 就是各自代码单元的位数。

UTF-16 的 16 指的就是最小为 16 位一个单元,也即两字节为一个单元,UTF-16 可以包含一个单元和两个单元,对应即是两个字节和四个字节。我们操作 UTF-16 时就是以它的一个单元为基本单位的。

你还记得你前几天被面试官说菜的时候学到的Unicode知识吗,在[面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜] “面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜”)这里面提到,UTF-16编码一个字符对于U+0000-U+FFFF范围内的字符采用2字节进行编码,而对于字符的码点大于U+FFFF的字符采用四字节进行编码,前者是两字节也就是一个代码单元,后者一个字符是四字节也就是两个代码单元!

而上面我的例子中的那个字符的Unicode值就是“U+1D11E”,这个Unicode的值明显大于U+FFFF,所以对于这个字符UTF-16需要使用四个字节进行编码,也就是使用两个代码单元!

所以你才看到我的上面那个示例结果表示一个字符的String.length()长度是2!

来看个例子!

public class testStringLength {
    public static void main(String [] args){
        String B = "𝄞"; // 这个就是那个音符字符,只不过由于当前的网页没支持这种编码,所以没显示。
        String C = "\uD834\uDD1E";// 这个就是音符字符的UTF-16编码
        System.out.println(C);
        System.out.println(B.length());
        System.out.println(B.codePointCount(0,B.length()));
        // 
    }
}

可以看到通过codePointCount()函数得知这个音乐字符是一个字符!

几个问题:0.codePointCount是什么意思呢?1.之前不是说音符字符是“U+1D11E”,为什么UTF-16是"uD834uDD1E",这俩之间如何转换?2.前面说了UTF-16的代码单元,UTF-32和UTF-8的代码单元是多少呢?

一个一个解答:

第0个问题:

codePointCount其实就是代码点数的意思,也就是一个字符就对应一个代码点数。

比如刚才音符字符(没办法打出来),它的代码点是U+1D11E,但它的代理单元是U+D834和U+DD1E,如果令字符串str = “u1D11E”,机器识别的不是音符字符,而是一个代码点”/u1D11“和字符”E“,所以会得到它的代码点数是2,代码单元数也是2。

但如果令字符str = “uD834uDD1E”,那么机器会识别它是2个代码单元代理,但是是1个代码点(那个音符字符),故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.

第1个问题

上图是对应的转换规则:

  • 首先 U+1D11E-U+10000 = U+0D11E
  • 接着将U+0D11E转换为二进制:0000 1101 0001 0001 1110,前10位是0000 1101 00 后10位是01 0001 1110
  • 接着套用模板:110110yyyyyyyyyy 110111xxxxxxxxxx
  • U+0D11E的二进制依次从左到右填入进模板:110110 0000 1101 00 110111 01 0001 1110
  • 然后将得到的二进制转换为16进制:d834dd1e,也就是你看到的utf-16编码了

第2个问题

  • 同理,UTF-32 以 32 位一个单元,它只包含这一种单元就够了,它的一单元自然也就是四字节了。
  • UTF-8 的 8 指的就是最小为 8 位一个单元,也即一字节为一个单元,UTF-8 可以包含一个单元,二个单元,三个单元及四个单元,对应即是一,二,三及四字节。

最后:

我们身为技术人员,最怕的就是安于现状,一直在原地踏步,那么你可能在30岁就会迎来自己的职业危机,因为你工作这么久提升的只有自己的年龄,技术还是万年不变!

我知道,对于一些学历没有优势的人来说,外包是别无选择,但是未来的路究竟要怎么走,取决你的步子迈多开。每个人都有自己的选择,如果你喜欢稳定,那按部就班适合你,但你有想法,不甘平庸,那就别让外包埋没了你。

如果你想在未来能够自我突破,圆梦大厂,那或许以上这份学习资料,你需要阅读阅读,希望能够对你的职业发展有所帮助。

最后,希望未来的我发展顺利,早日拿下p7!同样,也祝愿你实现自己的人生理想,愿我们都越来越好,共勉!

获取方式: 只需你**点赞+关注后,Java进阶交流群:714827309** 哦-!


获取方式: 只需你点赞+关注后,Java进阶交流群:714827309 进群拿资料哦-!

<think>我们来进行一场模拟面试。我会以面试官的身份提,涵盖Java后端开发常见的面试题。每个题后我会给出参考答案和解释,最后提供一组相关题供你继续练习。###1:HashMap的底层实现原理是什么?JDK8中做了哪些优化?**参考答案**:HashMap基于哈希表实现,使用数组+链表/红黑树存储数据。当添加元素时:1.计算键的hashCode,并通过扰动函数(高低位异或)和数组长度取模得到桶下标。2.若该桶为空,直接存入Node节点;否则遍历链表(或树):-若遇到相同键(hash相同且equals为true),则更新值。-否则将新节点插入链表尾部(JDK7是头插法,JDK8改为尾插法)。3.当链表长度≥8且数组长度≥64时,链表转为红黑树;当树节点数≤6时退化为链表。**JDK8优化**:-引入红黑树解决长链表性能退化题(O(n)到O(logn))-头插法改尾插法,避免多线程扩容时产生循环链表-扩容时优化:原链表拆分为高低位链表,直接分配到新数组位置```java//JDK8中putVal核心代码片段(简化版)finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){Node<K,V>[]tab;Node<K,V>p;intn,i;if((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;if((p=tab[i=(n-1)&hash])==null)//计算桶位置tab[i]=newNode(hash,key,value,null);else{//桶位已有数据,遍历链表/树//...省略具体处理逻辑}++modCount;if(++size>threshold)resize();returnnull;}```---###题2:如何理解Spring的IoC和AOP?请举例明其应用场景**参考答案**:**IoC(控制反转)**:-传统对象创建:开发者通过`new`主动实例化对象-IoC模式:将对象创建的控制权交给Spring容器,通过依赖注入(DI)实现-示例:Service层依赖DAO层对象,由Spring自动注入```java@ServicepublicclassUserServiceImplimplementsUserService{@Autowired//容器自动注入privateUserDaouserDao;}```**AOP(面向切面编程)**:-核心思想:将横切关注点(日志、事务等)与业务逻辑分离-实现方式:动态代理(JDK代理或CGLIB)-应用场景:-声明式事务管理(`@Transactional`)-统一日志记录-权限校验(如SpringSecurity的拦截器)```java@Aspect@ComponentpublicclassLogAspect{@Before("execution(*com.example.service.*.*(..))")publicvoidlogBefore(JoinPointjoinPoint){System.out.println("调用方法:"+joinPoint.getSignature().getName());}}```---###题3:MySQL的InnoDB索引为什么采用B+树而不是B树?**参考答案**:1.**磁盘I/O优化**:-B+树非叶子节点只存键值,不存数据,使得每个节点可容纳更多键值,降低树高度(通常3层可存百万级数据)-减少磁盘访次数(每次I/O读取一页数据)2.**范围查询高效**:-B+树叶节点形成有序链表,范围查询只需遍历链表-B树范围查询需中序遍历3.**全表扫描更优**:-B+树所有数据存储在叶节点,扫描全表只需遍历叶子节点链表-B树数据分布在各层,需全树遍历4.**查询稳定性**:-B+树任何查询都要到叶子节点,时间复杂度稳定为O(logn)-B树查询可能在非叶节点结束,不稳定---###题4:如何设计一个秒杀系统?请简述核心要点**参考答案**:1.**架构分层**:-客户端:静态资源CDN加速、按钮置灰防止重复提交-网关层:限流(如令牌桶)、恶意请求拦截-服务层:异步化(消息队列削峰)、无状态服务便于扩容-存储层:Redis集群缓存库存、数据库最终一致性2.**关键优化点**:-**库存预热**:提前将库存加载到Redis-**扣减库存**:```java//Redis原子操作保证并发安全Longresult=redisTemplate.execute("ifredis.call('get',KEYS[1])>'0'then"+"returnredis.call('decr',KEYS[1])"+"elsereturn-1end",Collections.singletonList("stock:"+skuId));```-**限流策略**:-入口层:Nginx限制IP并发连接数-服务层:Sentinel熔断降级-**消息队列**:下单请求写入RabbitMQ/Kafka,由消费者异步处理-**分布式事务**:本地消息表保证订单创建与库存扣减的最终一致---§§相关题§§:1.请解释ConcurrentHashMap如何保证线程安全?JDK7和JDK8实现有何不同?2.SpringBean的生命周期是怎样的?3.MySQL事务隔离级别有哪些?如何解决幻读题?4.在秒杀系统中,如何防止超卖?除了Redis还有哪些方案?</think>### Java后端开发模拟面试 #### **1:请解释Java中的垃圾回收机制,并描述G1收集器的工作原理** **回答**: Java垃圾回收(GC)自动管理堆内存,主要流程: 1. **标记阶段**:GC遍历对象图,标记存活对象(从GC Roots出发) 2. **清除阶段**:回收未被标记的对象内存(不同GC算法实现不同) 3. **压缩阶段**(可选):整理内存碎片(如SerialOld、ParallelOld) **G1收集器(Garbage-First)**: - **区域化堆内存**:将堆划分为2048个大小相等的Region(1MB~32MB) - **回收策略**: - **Young GC**:回收Eden/Survivor区(复制算法) - **Mixed GC**:回收部分老年代Region(标记-整理算法) - **核心流程**: 1. **初始标记**(STW):标记GC Roots直接关联对象 2. **并发标记**:与应用线程并发标记存活对象 3. **最终标记**(STW):处理SATB(Snapshot-At-The-Beginning)记录 4. **筛选回收**(STW):计算回收价值最高的Region,并行清理 ```java // 启用G1 GC的JVM参数示例 java -XX:+UseG1GC -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 -jar app.jar ``` --- #### **题2:如何设计一个线程安全的本地缓存?请用代码实现LRU策略** **要求**: - 支持并发读写 - 当缓存满时淘汰最近最少使用的条目 - 时间复杂度O(1)的get/put操作 **解决方案**: ```java import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; public class ThreadSafeLRUCache<K, V> { private final int capacity; private final ConcurrentHashMap<K, Node<K, V>> map; private final DoublyLinkedList<K, V> list; private final ReentrantLock lock = new ReentrantLock(); static class Node<K, V> { K key; V value; Node<K, V> prev; Node<K, V> next; } static class DoublyLinkedList<K, V> { private final Node<K, V> head; private final Node<K, V> tail; public DoublyLinkedList() { head = new Node<>(); tail = new Node<>(); head.next = tail; tail.prev = head; } // 添加节点到链表头部 public void addFirst(Node<K, V> node) { node.next = head.next; node.prev = head; head.next.prev = node; head.next = node; } // 移除节点 public void remove(Node<K, V> node) { node.prev.next = node.next; node.next.prev = node.prev; } // 移除尾节点(LRU淘汰) public Node<K, V> removeLast() { Node<K, V> last = tail.prev; if (last == head) return null; // 空链表 remove(last); return last; } } public ThreadSafeLRUCache(int capacity) { this.capacity = capacity; this.map = new ConcurrentHashMap<>(capacity); this.list = new DoublyLinkedList<>(); } public V get(K key) { Node<K, V> node = map.get(key); if (node == null) return null; lock.lock(); try { list.remove(node); list.addFirst(node); // 移动到头部表示最近使用 } finally { lock.unlock(); } return node.value; } public void put(K key, V value) { lock.lock(); try { // 如果key已存在,更新值并移到头部 if (map.containsKey(key)) { Node<K, V> node = map.get(key); node.value = value; list.remove(node); list.addFirst(node); return; } // 缓存满时淘汰LRU条目 if (map.size() >= capacity) { Node<K, V> lru = list.removeLast(); if (lru != null) { map.remove(lru.key); } } // 创建新节点并添加到头部 Node<K, V> newNode = new Node<>(); newNode.key = key; newNode.value = value; list.addFirst(newNode); map.put(key, newNode); } finally { lock.unlock(); } } } ``` **设计要点**: 1. **并发控制**: - `ConcurrentHashMap`保证读操作的并发性 - `ReentrantLock`确保链表操作的原子性 2. **LRU实现**: - 双向链表维护访顺序(头部最新,尾部最旧) - `get`操作将节点移到链表头部 - `put`满时移除尾部节点 3. **性能**: - HashMap提供O(1)访 - 链表操作O(1)时间复杂度 --- #### **题3:Spring Boot中如何实现接口幂等性?请给出分布式场景下的解决方案** **幂等性定义**:多次调用产生相同结果(如支付接口避免重复扣款) **解决方案**: ```java // 幂等性注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { long expireTime() default 3000; // 令牌有效期(ms) } // 幂等拦截器 @Component public class IdempotentInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { Method method = ((HandlerMethod) handler).getMethod(); if (!method.isAnnotationPresent(Idempotent.class)) return true; // 从Header获取幂等令牌 String token = request.getHeader("Idempotent-Token"); if (StringUtils.isEmpty(token)) { throw new BusinessException("缺少幂等令牌"); } // Redis原子删除令牌(相当于检查并消耗) String redisKey = "idempotent:" + token; Boolean deleted = redisTemplate.delete(redisKey); if (Boolean.TRUE.equals(deleted)) { return true; // 首次请求 } else { throw new BusinessException("重复请求"); // 令牌已消耗 } } } // 令牌生成接口 @RestController public class TokenController { @GetMapping("/token") public String generateToken() { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set("idempotent:" + token, "1", 3, TimeUnit.SECONDS); return token; } } // 使用示例 @Idempotent @PostMapping("/pay") public ResponseEntity<?> payment(@RequestBody PaymentRequest request) { // 业务逻辑... } ``` **工作流程**: 1. 客户端先请求`GET /token`获取有效期的令牌 2. 业务请求携带`Idempotent-Token`请求头 3. 拦截器通过Redis原子删除验证令牌: - 删除成功 → 首次请求 → 执行业务 - 删除失败 → 重复请求 → 直接拒绝 4. Redis保证分布式环境下的原子性 --- #### **题4:MySQL索引失效的常见场景有哪些?如何优化深分页查询?** **索引失效场景**: 1. **违反最左前缀原则**: ```sql -- 索引: (a,b,c) WHERE b = 2 AND c = 3; -- 失效(缺少a) ``` 2. **对索引列运算**: ```sql WHERE YEAR(create_time) = 2023; -- 失效 -- 优化: WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31' ``` 3. **使用`!=`或`NOT IN`**: ```sql WHERE status != 1; -- 可能全表扫描 ``` 4. **隐式类型转换**: ```sql -- 索引: varchar列 WHERE phone = 13800138000; -- 字符串转数字失效 ``` **深分页优化**: **题SQL**:`SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;` - **低效原因**:需先扫描1000010行再丢弃前100万行 **优化方案1:延迟关联** ```sql SELECT * FROM orders o JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) AS tmp ON o.id = tmp.id; -- 先查ID再回表 ``` **优化方案2:游标分页(推荐)** ```sql -- 第一页 SELECT * FROM orders ORDER BY id LIMIT 10; -- 下一页(传入上一页最后ID) SELECT * FROM orders WHERE id > ? -- 上一页最后一条记录的ID ORDER BY id LIMIT 10; ``` --- ### 面试总结 本次面试覆盖: - JVM垃圾回收机制(G1) - 并发数据结构设计(LRU缓存) - 分布式系统设计(接口幂等性) - 数据库性能优化(索引与分页)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值