分析java源码及大厂真题-基础
一、基础
1、String、Long源码解析和面试题
1.1、String
- 我们可以写一个demo来模仿一下:
String s = "hello"; s = "world";
-
我们可以写一个demo来模仿一下:
String str ="hello world !!"; // 这种写法是替换不掉的,必须接受 replace 方法返回的参数才行,这样才行:str = str.replace("l","dd"); str.replace("l","dd");
1.1.2 字符串乱码
- 我们可以写一个demo来模仿一下字符乱码:
String str ="nihao 你好 喬亂"; // 字符串转化成 byte 数组 byte[] bytes = str.getBytes("ISO-8859-1"); // byte 数组转化成字符串 String s2 = new String(bytes); log.info(s2); // 结果打印为: nihao ?? ??
1.1.3 首字母大小写
1.1.4 相等判断
- 我们可以写一个demo来看一下:
public boolean equals(Object anObject) { // 判断内存地址是否相同 if (this == anObject) { return true; } // 待比较的对象是否是 String,如果不是 String,直接返回不相等 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; // 两个字符串的长度是否相等,不等则直接返回不相等 if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; // 依次比较每个字符是否相等,若有一个不等,直接返回不相等 while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
1.1.5 替换、删除
-
写一个演示demo:
public void testReplace(){ String str ="hello word !!"; log.info("替换之前 :{}",str); str = str.replace('l','d'); log.info("替换所有字符 :{}",str); str = str.replaceAll("d","l"); log.info("替换全部 :{}",str); str = str.replaceFirst("l",""); log.info("替换第一个 l :{}",str); } //输出的结果是: 替换之前 :hello word !! 替换所有字符 :heddo word !! 替换全部 :hello worl !! 替换第一个 :helo worl !!
1.1.6 拆分和合并
- 写一个演示demo:
String s ="boo:and:foo"; // 我们对 s 进行了各种拆分,演示的代码和结果是: s.split(":") 结果:["boo","and","foo"] s.split(":",2) 结果:["boo","and:foo"] s.split(":",5) 结果:["boo","and","foo"] s.split(":",-2) 结果:["boo","and","foo"] s.split("o") 结果:["b","",":and:f"] s.split("o",2) 结果:["b","o:and:foo"]
- 写一个演示demo:
String a =",a,,b,"; a.split(",") 结果:["","a","","b"]
- 写一个演示demo:
String a =",a, , b c ,"; // Splitter 是 Guava 提供的 API List<String> list = Splitter.on(',') .trimResults()// 去掉空格 .omitEmptyStrings()// 去掉空值 .splitToList(a); log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list)); // 打印出的结果为: ["a","b c"]
-
写一个演示demo:
// 依次 join 多个字符串,Joiner 是 Guava 提供的 API Joiner joiner = Joiner.on(",").skipNulls(); String result = joiner.join("hello",null,"china"); log.info("依次 join 多个字符串:{}",result); List<String> list = Lists.newArrayList(new String[]{"hello","china",null}); log.info("自动删除 list 中空值:{}",joiner.join(list)); // 输出的结果为; 依次 join 多个字符串:hello,china 自动删除 list 中空值:hello,china
1.2、Long
1.2.1 缓存
private static class LongCache {
private LongCache(){}
// 缓存,范围从 -128 到 127,+1 是因为有个 0
static final Long cache[] = new Long[-(-128) + 127 + 1];
// 容器初始化时,进行加载
static {
// 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
1.3、面试题
1.3.1 为什么使用 Long 时,大家推荐多使用 valueOf 方法,少使用 parseLong 方法
答:因为 Long 本身有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法就没有这个机制。
1.3.2 如何解决 String 乱码的问题
答:乱码的问题的根源主要是两个:字符集不支持复杂汉字、二进制进行转化时字符集不匹配,所以在 String 乱码时我们可以这么做:
1. 所有可以指定字符集的地方强制指定字符集,比如 new String 和getBytes 这两个地方;
2. 我们应该使用 UTF-8 这种能完整支持复杂汉字的字符集。
1.3.3 为什么大家都说 String 是不可变的
答:主要是因为 String 和保存数据的 char 数组,都被 final 关键字所修饰,所以是不可变的,具体细节描述可以参考上文。
1.3.4 String 一些常用操作问题,如问如何分割、合并、替换、删除、截取等等问题
答:这些都属于问 String 的基本操作题目,考察我们平时对 String 的使用熟练程度,可以参考上文。
2、Java 常用关键字理解
2.1、static
2.1.1 修饰的对象
所以在使用 static 修饰类变量时,如何保证线程安全是我们常常需要考虑的。 当 static
修饰方法时,代表该方法和当前类是无关的,任意类都可以直接访问(如果权限是public 的话)。
-
当 static 修饰方法块时,我们叫做静态块,静态块常常用于在类启动之前,初始化一些值,比如:
public static List<String> list = new ArrayList(); // 进行一些初始化的工作 static { list.add("1"); }
2.1.2 初始化时机
- 对于被static修饰的类变量、方法块和静态方法的初始化机会,我们写一个测试demo:
被 static 修饰的方法,在类初始化的时候并不会初始化,只有当自己被调用时,才会被执行。
2.1.3 final
2.1.4 try、catch、finally
-
演示demo:
public void testCatchFinally() { try { log.info("try is run"); if (true) { throw new RuntimeException("try exception"); } } catch (Exception e) { log.info("catch is run"); if (true) { throw new RuntimeException("catch exception"); } } finally { log.info("finally is run"); } }
2.1.5 volatile
volatile 的意思是可见的,常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知到其它线程上,其它线程就能知道当前共享变量的值已经被修改了。
我们再说原理之前,先说下基础知识。就是在多核 CPU 下,为了提高效率,线程在拿值时,是直接和 CPU 缓存打交道的,而不是内存。主要是因为 CPU 缓存执行速度更快,比如线程要拿
值 C,会直接从 CPU 缓存中拿, CPU 缓存中没有,就会从内存中拿,所以线程读的操作永远都是拿 CPU 缓存的值。
2.1.6 transient
transient 关键字我们常用来修饰类变量,意思是当前变量是无需进行序列化的。在序列化时,
就会忽略该变量,这些在序列化工具底层,就已经对 transient 进行了支持。
2.1.7 default
- default 关键字一般会用在接口的方法上,意思是对于该接口,子类是无需强制实现的,但自己必须有默认实现,我们举个例子如下:
2.2、面试题
2.2.1 如何证明 static 静态变量和类无关?
答:从三个方面就可以看出静态变量和类无关。
1. 我们不需要初始化类就可直接使用静态变量;
2. 我们在类中写个 main 方法运行,即便不写初始化类的代码,静态变量都会自动初始化;
3. 静态变量只会初始化一次,初始化完成之后,不管我再 new 多少个类出来,静态变量都不会
再初始化了。
不仅仅是静态变量,静态方法块也和类无关。
2.2.2 常常看见变量和方法被 static 和 final 两个关键字修饰,为什么这么做?
答:这么做有两个目的:
1. 变量和方法于类无关,可以直接使用,使用比较方便;
2. 强调变量内存地址不可变,方法不可继承覆写,强调了方法内部的稳定性。
2.2.3 catch 中发生了未知异常,finally 还会执行么?
答:会的,catch 发生了异常,finally 还会执行的,并且是 finally 执行完成之后,才会抛出
catch 中的异常。
不过 catch 会吃掉 try 中抛出的异常,为了避免这种情况,在一些可以预见 catch 中会发生异
常的地方,先把 try 抛出的异常打印出来,这样从日志中就可以看到完整的异常了。
2.2.4 volatile 关键字的作用和原理
答:这个上文说的比较清楚,可以参考上文。
3、Arrays、Collections、Objects 常用方法源码解析
3.1、工具类通用的特征
再看细节之前,我们先总结一下好的工具类都有哪些通用的特征写法:
- 构造器必须是私有的。这样的话,工具类就无法被 new 出来,因为工具类在使用的时候,无
需初始化,直接使用即可,所以不会开放出构造器出来。 - 工具类的工具方法必须被 static、final 关键字修饰。这样的话就可以保证方法不可变,并且可以直接使用,非常方便。
我们需要注意的是,尽量不在工具方法中,对共享变量有做修改的操作访问(如果必须要做的话,必须加锁),因为会有线程安全的问题。除此之外,工具类方法本身是没有线程安全问题的,可以放心使用。
3.2、Arrays
Arrays 主要对数组提供了一些高效的操作,比如说排序、查找、填充、拷贝、相等判断等等。
我们选择其中两三看下,对其余操作感兴趣的同学可以到 GitHub 上查看源码解析。
3.2.1 排序
Arrays.sort 方法主要用于排序,入参支持 int、long、double 等各种基本类型的数组,也支持
自定义类的数组,下面我们写个 demo 来演示一下自定义类数组的排序:
-
写个demo:
@Data // 自定义类 class SortDTO { private String sortTarget; public SortDTO(String sortTarget) { this.sortTarget = sortTarget; } } @Test public void testSort(){ List<SortDTO> list = ImmutableList.of( new SortDTO("300"), new SortDTO("50"), new SortDTO("200"), new SortDTO("220") ); // 我们先把数组的大小初始化成 list 的大小,保证能够正确执行 toArray SortDTO[] array = new SortDTO[list.size()]; list.toArray(array); log.info("排序之前:{}", JSON.toJSONString(array)); Arrays.sort(array, Comparator.comparing(SortDTO::getSortTarget)); log.info("排序之后:{}", JSON.toJSONString(array)); } 输出结果为: 排序之前:[{"sortTarget":"300"},{"sortTarget":"50"},{"sortTarget":"200"},{"sortTarget":"220"}] 排序之后:[{"sortTarget":"200"},{"sortTarget":"220"},{"sortTarget":"300"},{"sortTarget":"50"}]
3.2.2 二分查找法
Arrays.binarySearch 方法主要用于快速从数组中查找出对应的值。其支持的入参类型非常多,
如 byte、int、long 各种类型的数组。返回参数是查找到的对应数组下标的值,如果查询不
到,则返回负数。
-
我们写一个demo:
List<SortDTO> list = ImmutableList.of( new SortDTO("300"), new SortDTO("50"), new SortDTO("200"), new SortDTO("220") ); SortDTO[] array = new SortDTO[list.size()]; list.toArray(array); log.info("搜索之前:{}", JSON.toJSONString(array)); Arrays.sort(array, Comparator.comparing(SortDTO::getSortTarget)); log.info("先排序,结果为:{}", JSON.toJSONString(array)); int index = Arrays.binarySearch(array, new SortDTO("200"), Comparator.comparing(SortDTO::getSortTarget)); if(index<0){ throw new RuntimeException("没有找到 200"); } log.info("搜索结果:{}", JSON.toJSONString(array[index])); 输出的结果为: 搜索之前:[{"sortTarget":"300"},{"sortTarget":"50"},{"sortTarget":"200"},{"sortTarget":"220"}] 先排序,结果为:[{"sortTarget":"200"},{"sortTarget":"220"},{"sortTarget":"300"},{"sortTarget":"5 搜索结果:{"sortTarget":"200"}
-
这种情况进行了判断,如果是负数,会提前抛出明确的异常。 接下来,我们来看下二分法底层代码的实现:
// a:我们要搜索的数组,fromIndex:从那里开始搜索,默认是0; toIndex:搜索到何时停止,默认 // key:我们需要搜索的值 c:外部比较器 private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex, T key, Comparator<? super T> c) { // 如果比较器 c 是空的,直接使用 key 的 Comparable.compareTo 方法进行排序 // 假设 key 类型是 String 类型,String 默认实现了 Comparable 接口,就可以直接使用 compar if (c == null) { // 这是另外一个方法,使用内部排序器进行比较的方法 return binarySearch0(a, fromIndex, toIndex, key); } int low = fromIndex; int high = toIndex - 1; // 开始位置小于结束位置,就会一直循环搜索 while (low <= high) { // 假设 low =0,high =10,那么 mid 就是 5,所以说二分的意思主要在这里,每次都是计算索 int mid = (low + high) >>> 1; T midVal = a[mid]; // 比较数组中间值和给定的值的大小关系 int cmp = c.compare(midVal, key); // 如果数组中间值小于给定的值,说明我们要找的值在中间值的右边 if (cmp < 0) low = mid + 1; // 我们要找的值在中间值的左边 else if (cmp > 0) high = mid - 1; else // 找到了 return mid; // key found } // 返回的值是负数,表示没有找到 return -(low + 1); // key not found.
3.2.3 拷贝
数组拷贝我们经常遇到,有时需要拷贝整个数组,有时需要拷贝部分,比如 ArrayList 在
add(扩容) 或 remove(删除元素不是最后一个) 操作时,会进行一些拷贝。拷贝整个数组
我们可以使用 copyOf 方法,拷贝部分我们可以使用 copyOfRange 方法,以 copyOfRange
为例,看下底层源码的实现:
-
写个demo:
// original 原始数组数据 // from 拷贝起点 // to 拷贝终点 public static char[] copyOfRange(char[] original, int from, int to) { // 需要拷贝的长度 int newLength = to - from; if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); // 初始化新数组 char[] copy = new char[newLength]; // 调用 native 方法进行拷贝,参数的意思分别是: // 被拷贝的数组、从数组那里开始、目标数组、从目的数组那里开始拷贝、拷贝的长度 System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); return copy; }
3.3、Collections
3.3.1 集合和中最大、小值
3.3.2 多种类型的集合
Collections 对原始集合类进行了封装,提供了更好的集合类给我们,一种是线程安全的集合,一种是不可变的集合,针对 List、Map、Set 都有提供,我们先来看下线程安全的集合:线程安全的集合方法都是 synchronized 打头的,如下:
- 从方法命名我们都可以看出来,底层是通过 synchronized 轻量锁来实现的,我们以synchronizedList 为例来说明下底层的实现:
可以看到 List 的所有操作方法都被加上了 synchronized 锁,所以多线程对集合同时进行操
作,是线程安全的。
3.3.2.1 不可变集合
得到不可变集合的方法都是以 unmodifiable 开头的。这类方法的意思是,我们会从原集合中,得到一个不可变的新集合,新集合只能访问,无法修改;一旦修改,就会抛出异常。这主要是因
为只开放了查询方法,其余任何修改操作都会抛出异常,我们以 unmodifiableList 为例来看下底层实现机制:
3.4、Objects
对于 Objects,我们经常使用的就是两个场景,相等判断和判空。
3.4.1 相等判断
Objects 有提供 equals 和 deepEquals 两个方法来进行相等判断,前者是判断基本类型和自定
义类的,后者是用来判断数组的,我们来看下底层的源码实现:
从源码中,可以看出 Objects 对基本类型和复杂类型的对象,都有着比较细粒度的判断,可以
放心使用。
3.4.2 为空判断
Objects 提供了各种关于空的一些判断,isNull 和 nonNull 对于对象是否为空返回 Boolean
值,requireNonNull 方法更加严格,如果一旦为空,会直接抛出异常,我们需要根据生活的场
景选择使用。
3.5、面试题
3.5.1 工作中有没有遇到特别好用的工具类,如何写好一个工具类
答:有的,像 Arrays 的排序、二分查找、Collections 的不可变、线程安全集合类、Objects
的判空相等判断等等工具类,好的工具类肯定很好用,比如说使用 static final 关键字对方法进
行修饰,工具类构造器必须是私有等等手段来写好工具类。
3.5.2 写一个二分查找算法的实现
答:可以参考 Arrays 的 binarySearch 方法的源码实现。
3.5.3 如果我希望 ArrayList 初始化之后,不能被修改,该怎么办
答:可以使用 Collections 的 unmodifiableList 的方法,该方法会返回一个不能被修改的内部
类集合,这些集合类只开放查询的方法,对于调用修改集合的方法会直接抛出异常。