总结
我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。
这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。
大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:
希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!
public char charAt(int index) {
//1. 首先判断是否越界
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
//2. 返回相应位置的值
return value[index];
}
4.提取子串
用String类的substring方法可以提取字符串中的子串,简单分析一下substring(int beginIndex, int endIndex)吧,该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个 新的字符串(重新new了一个String) 返回.
方法内部是将数组进行部分复制完成的,所以该方法不会对原有的数组进行更改.
public String substring(int beginIndex, int endIndex) {
//1.验证入参是否越界
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
//2. 记录切割长度
int subLen = endIndex - beginIndex;
//3. 入参合法性
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//4. 如果开始切割处是0,结束切割处是value数组长度,那么相当于没有切割嘛,就直接返回原字符串;如果是其他情况:则重新新建一个String对象
-
return ((beginIndex == 0) && (endIndex == value.length)) ? this
- new String(value, beginIndex, subLen);
}
/**
- 通过一个char数组复制部分内容生成一个新数组,复制区间:从offset到offset+count处.
*/
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
//count==0
if (offset <= value.length) {
this.value = “”.value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//复制一部分
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
5.字符串比较
- compareTo(String anotherString)
该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系.若当前对象比参数大则返回正整数,反之返回负整数,相等返回0.
主要是挨个字符进行比较
public int compareTo(String anotherString) {
//1. 记录长度
int len1 = value.length;
int len2 = anotherString.value.length;
//2. 最短长度
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
//3. 循环逐个字符进行比较,如果不相等则返回字符之差
//这里只需要循环lim次就行了
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
//可能是正数或负数
return c1 - c2;
}
k++;
}
//4. 最后返回长度之差 这里可能是0,即相等
return len1 - len2;
}
- compareToIgnore(String anotherString)
与compareTo()方法相似,但忽略大小写.
实现:从下面的源码可以看出,最终实现是通过一个内部类CaseInsensitiveComparator,它实现了Comparator和Serializable接口,并实现了compare()方法,里面的实现方法和上面的compareTo()方法差不多,只不过忽略大小写.
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
public static final Comparator CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
- equals(Object anotherObject)
比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false.
大体实现思路:
-
先判断引用是否相同
-
再判断该Object对象是否是String的实例
-
再判断两个字符串的长度是否一致
-
最后挨个字符进行比较
public boolean equals(Object anObject) {
//1. 引用相同
if (this == anObject) {
return true;
}
//2. 是String的实例?
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//3. 长度
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//4. 挨个字符进行比较
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- equalsIgnoreCase(String anotherString)
与equals方法相似,但忽略大小写.但是这里要稍微复杂一点,因为牵连到另一个方法regionMatches(),没关系,下面跟着我一起慢慢分析.
在equalsIgnoreCase()方法里面首先是校验引用值是否一致,再判断否为空,紧接着判断长度是否一致,最后通过regionMatches()方法测试两个字符串每个字符是否相等(忽略大小写).
在regionMatches()方法中其实还是比较简单的,就是逐字符进行比较,当需要进行忽略大小写时,如果遇到不相等的2字符,先统一转成大写进行比较,如果相同则继续比较下一个,不相同则转成小写再判断是否一致.
public boolean equalsIgnoreCase(String anotherString) {
-
return (this == anotherString) ? true
- (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
// Note: toffset, ooffset, or len might be near -1>>>1.
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
//循环校验每个字符是否相等,相等则继续校验下一个字符
char c1 = ta[to++];
char c2 = pa[po++];
if (c1 == c2) {
continue;
}
//如果遇到不相等的2字符,再判断是否忽略大小写.
//先统一转成大写进行比较,如果相同则继续比较下一个,不相同则转成小写再判断是否一致
if (ignoreCase) {
// If characters don’t match but case may be ignored,
// try converting both characters to uppercase.
// If the results match, then the comparison scan should
// continue.
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 == u2) {
continue;
}
// Unfortunately, conversion to uppercase does not work properly
// for the Georgian alphabet, which has strange rules about case
// conversion. So we need to make one last check before
// exiting.
if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
return false;
}
return true;
}
6.字符串连接
将指定字符串联到此字符串的结尾,效果等价于”+”.
实现思路:构建一个新数组,先将原来的数组复制进新数组里面,再将需要连接的字符串复制进新数组里面(存放到后面).
public String concat(String str) {
//1. 首先获取传入字符串长度 咦,居然没有对入参合法性进行判断?万一是null呢
int otherLen = str.length();
//2. 如果传入字符串长度为0,就没必要往后面走了
if (otherLen == 0) {
return this;
}
//3. 记录当前数组长度
int len = value.length;
//4. 搞一个新数组(空间大小为len + otherLen),前面len个空间用来存放value数组
char buf[] = Arrays.copyOf(value, len + otherLen);
//5. 将str存入buf数组的后面otherLen个空间里面
str.getChars(buf, len);
//6. new一个String将新建的buf数组传入
return new String(buf, true);
}
//将des数组复制进value数组中,dstBegin:目的数组放置的起始位置
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
//这里的share参数貌似总是为true 所以是暂时没用咯??
String(char[] value, boolean share) {
// assert share : “unshared not supported”;
this.value = value;
}
7.字符串中单个字符查找
- indexOf(int ch/String str)
返回指定字符在此字符串中第一次出现处的索引,在该对象表示的字符序列中第一次出现该字符的索引,如果未出现该字符,则返回 -1。
其实该方法最后是调用的indexOf(ch/str, 0);
该方法放到下面进行分析.
- indexOf(int ch/String str, int fromIndex)
该方法与第一种类似,区别在于该方法从fromIndex位置向后查找.
先分析indexOf(int ch, int fromIndex),该方法是查找ch在fromIndex索引之后第一次出现的索引.主要就是逐个字符进行比较,相同则返回索引.如果未找到则返回-1.
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
//1. 边界
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
//2. 是否是罕见字符
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
//在这里其实已经处理了大多数情况
final char[] value = this.value;
//3. 从fromIndex开始,循环,找到第一个与ch相等的进行返回
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
//4. 罕见字符处理
return indexOfSupplementary(ch, fromIndex);
}
}
再来分析indexOf(String str, int fromIndex),该方法功能是从指定的索引处开始,返回第一次出现的指定子字符串在此字符串中的索引.
大体思路:
-
有点类似于字符串查找子串,先在当前字符串中找到与目标字符串的第一个字符相同的索引处
-
再从此索引出发循环遍历目标字符串后面的字符.
-
如果全部相同,则返回下标;如果不全部相同,则重复步骤1
文字可能描述不清楚,上图片
我们要在beautifulauful中查找ful,那么步骤是首先找到f,再匹配后面的ul
部分,找到则返回索引,未找到则继续查找.
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
//在source数组中查找target数组
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
//1. 校验参数合法性
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
//2. 记录第一个需要匹配的字符
char first = target[targetOffset];
//3. 这一次匹配的能到达的最大索引
int max = sourceOffset + (sourceCount - targetCount);
//4. 循环遍历后面的数组
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
//5. 循环查找,直到查找到第一个和目标字符串第一个字符相同的索引
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
//6. 找到了第一个字符,再来看看目标字符串剩下的部分
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
//7. 匹配一下目标字符串后面的字符串是否相等 不相等的时候就跳出循环
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
//8. 如果全部相等,则返回索引
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
- lastIndexOf(int ch/String str)
该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找.
实现方法也与第一种是类似的,只不过是从后往前查找.
public int lastIndexOf(int ch) {
return lastIndexOf(ch, value.length - 1);
}
public int lastIndexOf(int ch, int fromIndex) {
//1. 判断是否是罕见字符
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
//2. 从fromIndex(从哪个索引开始), value.length - 1(数组最后一个索引)中小一点的往前找,这里之所以这样做是因为fromIndex可能比value.length-1大.这里求最小值就可以覆盖所有情况,不管fromIndex和value.length-1谁大.
int i = Math.min(fromIndex, value.length - 1);
//3. 循环 逐个字符进行比较,找到则返回索引
for (; i >= 0; i–) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
//4. 罕见字符处理
return lastIndexOfSupplementary(ch, fromIndex);
}
}
- lastIndexOf(int ch/String str, int fromIndex)
该方法与第二种方法类似,区别在于该方法从fromIndex位置向前查找.
实现思路:这里要稍微复杂一点,相当于从后往前查找指定子串.上图吧
图画的有点丑,哈哈. 假设我们需要在StringBuffer
中查找ABuff
中的子串Buff
,因为Buff
的长度是4,所以我们最大的索引可能值是图中的rightIndex.然后我们就开始在source数组中匹配目标字符串的最后一个字符,匹配到后,再逐个字符进行比较剩余的字符,如果全部匹配,则返回索引.未全部匹配,则再次在source数组中寻找与目标字符串最后一个字符相等的字符,然后找到后继续匹配除去最后一个字符剩余的字符串. 唉~叙述的不是特别清晰,看代码吧,代码比我说的清晰…
public int lastIndexOf(String str, int fromIndex) {
return lastIndexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
static int lastIndexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
/*
-
Check arguments; return immediately where possible. For
-
consistency, don’t check for null str.
*/
//1. 最大索引的可能值
int rightIndex = sourceCount - targetCount;
//2. 参数合法性检验
if (fromIndex < 0) {
return -1;
}
if (fromIndex > rightIndex) {
fromIndex = rightIndex;
}
/* Empty string always matches. */
if (targetCount == 0) {
return fromIndex;
}
//3. 记录目标字符串最后一个字符索引处和该字符内容
int strLastIndex = targetOffset + targetCount - 1;
char strLastChar = target[strLastIndex];
//4. 只需要遍历到min处即可停止遍历了,因为在min前面的字符数量已经小于目标字符串的长度了
int min = sourceOffset + targetCount - 1;
//5. strLastChar在source中的最大索引
int i = min + fromIndex;
//这里的语法不是很常见,有点类似于goto,平时我们在使用时尽量不采用这种方式,这种方式容易降低代码的可读性,而且容易出错.
startSearchForLastChar:
while (true) {
//6. 在有效遍历区间内,循环查找第一个与目标字符串最后一个字符相等的字符,如果找到,则跳出循环,该字符的索引是i
while (i >= min && source[i] != strLastChar) {
i–;
}
//7. 如果已经小于min了,那么说明没找到,直接返回-1
if (i < min) {
return -1;
}
//8. 找到了,则再进行查找目标字符串除去最后一个字符剩下的子串
//从最后一个字符的前一个字符开始查找
int j = i - 1;
//9. 目标字符串除去最后一个字符剩下的子串长度是targetCount - 1,此处start是此次剩余子串查找能到达的最小索引处
int start = j - (targetCount - 1);
//10. 记录目标字符串的倒数第二个字符所在target中的索引
int k = strLastIndex - 1;
//11. 循环查找剩余子串是否全部字符相同
//不相同则直接跳出继续第6步
//全部相同则返回索引
while (j > start) {
if (source[j–] != target[k–]) {
i–;
continue startSearchForLastChar;
}
}
return start - sourceOffset + 1;
}
}
8.字符串中字符的替换
- replace(char oldChar, char newChar)
功能:用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串.
大体思路:
-
首先判断oldChar与newChar是否相同,相同的话就没必要进行后面的操作了
-
从最前面开始匹配与oldChar相匹配的字符,记录索引为i
-
如果上面的i是正常范围内(小于len),新建一个数组,长度为len(原来的字符串的长度),将i索引前面的字符逐一复制进新数组里面,然后循环
i<=x<len
的字符,将字符逐一复制进新数组,但是这次的复制有规则,即如果那个字符与oldChar相同那么新数组对应索引处就放newChar. -
最后通过新建的数组new一个String对象返回
思考:一开始我觉得第二步好像没什么必要性,没有第二步其实也能实现.但是,仔细想想,假设原字符串没有查找到与oldChar匹配的字符,那么我们就可以规避去新建一个数组,从而节约了不必要的开销.可以,很棒,我们就是要追求极致的性能,减少浪费资源.
小细节:源码中有一个小细节,注释中有一句avoid getfield opcode
,意思是避免getfield操作码?
感觉那句代码就是拷贝了一个引用副本啊,有什么高大上的作用?查阅文章https://blog.csdn.net/gaopu12345/article/details/52084218 后发现答案:在一个方法中需要大量引用实例域变量的时候,使用方法中的局部变量代替引用可以减少getfield操作的次数,提高性能。
public String replace(char oldChar, char newChar) {
//1. 如果两者相同,那么就没必要进行比较了
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
//2. 从最前面开始,循环遍历,找到与oldChar相同的字符
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
//3. 如果找到了与oldChar相同的字符才进入if
if (i < len) {
//4. 新建一个数组,用于存放新数据
char buf[] = new char[len];
//5. 将i前面的全部复制进新数组里面去
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
//6. 在i后面的字符,我们将其一个一个地放入新数组中,当然在放入时需要比对是否和oldChar相同,相同则存放newChar
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//7. 最终重新new一个String
return new String(buf, true);
}
}
return this;
}
9.其他类方法
- trim()
功能:截去字符串两端的空格,但对于中间的空格不处理
大体实现:记录前面有st个空格,最后有多少个空格,那么长度就减去多少个空格,最后根据上面的这2个数据去切割字符串.
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
//1. 记录前面有多少个空格
while ((st < len) && (val[st] <= ’ ')) {
st++;
}
//2. 记录后面有多少个空格
while ((st < len) && (val[len - 1] <= ’ ')) {
len–;
}
//3. 切割呗,注意:切割里面具体实现是重新new了一个String
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
- startsWith(String prefix)或endsWith(String suffix)
功能:用来比较当前字符串的起始字符或子字符串prefix和终止字符或子字符串suffix是否和当前字符串相同,重载方法中同时还可以指定比较的开始位置offset.
思路:比较简单,就直接看代码了,有详细注释.
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
//1. 入参检测合法性
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
//2. 循环进行逐个字符遍历,有不相等的就直接返回false,遍历完了还没发现不相同的,那么就是true
while (–pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
- contains(String str)
功能:判断参数s是否被包含在字符串中,并返回一个布尔类型的值.
思路:其实就是利用已经实现好的indexOf()去查找是否包含.源码中对于已实现的东西利用率还是非常高的.我们要多学习.
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
10.基本类型转换为字符串类型
这部分代码一看就懂,都是一句代码解决.
public static String valueOf(Object obj) {
return (obj == null) ? “null” : obj.toString();
}
public static String valueOf(char data[]) {
return new String(data);
}
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[]) {
return new String(data);
}
public static String valueOf(boolean b) {
return b ? “true” : “false”;
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}
注意事项
结语
小编也是很有感触,如果一直都是在中小公司,没有接触过大型的互联网架构设计的话,只靠自己看书去提升可能一辈子都很难达到高级架构师的技术和认知高度。向厉害的人去学习是最有效减少时间摸索、精力浪费的方式。
我们选择的这个行业就一直要持续的学习,又很吃青春饭。
虽然大家可能经常见到说程序员年薪几十万,但这样的人毕竟不是大部份,要么是有名校光环,要么是在阿里华为这样的大企业。年龄一大,更有可能被裁。
送给每一位想学习Java小伙伴,用来提升自己。
本文到这里就结束了,喜欢的朋友可以帮忙点赞和评论一下,感谢支持!
ring(data);
}
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[]) {
return new String(data);
}
public static String valueOf(boolean b) {
return b ? “true” : “false”;
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}
注意事项
结语
小编也是很有感触,如果一直都是在中小公司,没有接触过大型的互联网架构设计的话,只靠自己看书去提升可能一辈子都很难达到高级架构师的技术和认知高度。向厉害的人去学习是最有效减少时间摸索、精力浪费的方式。
我们选择的这个行业就一直要持续的学习,又很吃青春饭。
虽然大家可能经常见到说程序员年薪几十万,但这样的人毕竟不是大部份,要么是有名校光环,要么是在阿里华为这样的大企业。年龄一大,更有可能被裁。
送给每一位想学习Java小伙伴,用来提升自己。
[外链图片转存中…(img-cJ5Qp7xR-1715336801250)]
本文到这里就结束了,喜欢的朋友可以帮忙点赞和评论一下,感谢支持!