2024年安卓最全从源码角度彻底搞懂String、StringBuffer、StringBuilder,2024年最新ios大厂面试

结语

看到这篇文章的人不知道有多少是和我一样的Android程序员。

35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

//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字符,并返回一个新的字符串.

大体思路:

  1. 首先判断oldChar与newChar是否相同,相同的话就没必要进行后面的操作了

  2. 从最前面开始匹配与oldChar相匹配的字符,记录索引为i

  3. 如果上面的i是正常范围内(小于len),新建一个数组,长度为len(原来的字符串的长度),将i索引前面的字符逐一复制进新数组里面,然后循环 i<=x<len 的字符,将字符逐一复制进新数组,但是这次的复制有规则,即如果那个字符与oldChar相同那么新数组对应索引处就放newChar.

  4. 最后通过新建的数组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);

}

注意事项

最后注意一下:Android 6.0(23) 源码中,String类的实现被替换了,具体调用的时候,会调用一个StringFactory来生成一个String.

来看下Android源码中String,我擦,这……直接抛错误UnsupportedOperationException,可能是因为Oracle告Google的原因吧…

public String() {

throw new UnsupportedOperationException(“Use StringFactory instead.”);

}

public String(String original) {

throw new UnsupportedOperationException(“Use StringFactory instead.”);

}

我们平时开发APP时都是使用的java.lang包下面的String,上面的问题一般不会遇到,但是作为Android开发者还是要了解一下.

三、AbstractStringBuilder源码分析


先看看类StringBuffer和StringBuilder的继承结构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到StringBuffer和StringBuilder都是继承了AbstractStringBuilder.所以这里先分析一下AbstractStringBuilder.

在这基类里面真实的保存了StringBuffer和StringBuilder操作的实际数据内容,数据内容其实是一个char[] value;数组,在其构造方法中其实就是初始化该字符数组.

char[] value;

AbstractStringBuilder() {

}

AbstractStringBuilder(int capacity) {

value = new char[capacity];

}

1.扩容

既然数据内容(上面的value数组)是在AbstractStringBuilder里面的,那么很多操作我觉得应该也是在父类里面,比如扩容,下面我们看看源码

public void ensureCapacity(int minimumCapacity) {

if (minimumCapacity > 0)

ensureCapacityInternal(minimumCapacity);

}

/**

  • 确保value字符数组不会越界.重新new一个数组,引用指向value

*/

private void ensureCapacityInternal(int minimumCapacity) {

// overflow-conscious code

if (minimumCapacity - value.length > 0) {

value = Arrays.copyOf(value,

newCapacity(minimumCapacity));

}

}

/**

  • 扩容:之前的大小的2倍+2

*/

private int newCapacity(int minCapacity) {

// overflow-conscious code 扩大2倍+2

//小知识点:这里可能会溢出,溢出后是负数哈,注意

int newCapacity = (value.length << 1) + 2;

if (newCapacity - minCapacity < 0) {

newCapacity = minCapacity;

}

//MAX_ARRAY_SIZE的值是Integer.MAX_VALUE - 8,先判断一下预期容量(newCapacity)是否在0<x<MAX_ARRAY_SIZE之间,在这区间内就直接将数值返回,不在这区间就去判断一下是否溢出

return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)

? hugeCapacity(minCapacity)
newCapacity;

}

/**

  • 判断大小 是否溢出

*/

private int hugeCapacity(int minCapacity) {

if (Integer.MAX_VALUE - minCapacity < 0) { // overflow

throw new OutOfMemoryError();

}

return (minCapacity > MAX_ARRAY_SIZE)

? minCapacity : MAX_ARRAY_SIZE;

}

可以看到这里的扩容方式是 = 以前的大小*2+2,其他的细节方法中已给出详细注释.

2.追加

举一个比较有代表性的添加,详细注释在代码中

/**

  • 追加:从指定字符串的片段

*/

public AbstractStringBuilder append(CharSequence s, int start, int end) {

//1. 如果是空,则添加字符串"null"

if (s == null)

s = “null”;

//2. 判断是否越界

if ((start < 0) || (start > end) || (end > s.length()))

throw new IndexOutOfBoundsException(

"start " + start + ", end " + end + ", s.length() "

  • s.length());

//3. 记录添加字符串长度

int len = end - start;

//4. 判断一下 当前数组长度+需要添加的字符串长度 是否够装,不够装就扩容(扩容时还有复制原内容到新数组中)

ensureCapacityInternal(count + len);

//5. 追加内容到value数组最后

for (int i = start, j = count; i < end; i++, j++)

value[j] = s.charAt(i);

//6. 更新数组长度

count += len;

return this;

}

3.增加

这里的大体思想是和以前大一的时候用C语言在数组中插入数据是一样的.

这里假设需要插入的字符串s,插入在目标字符串desOffset处,插入的长度是len.首先将需要插入处的desOffset~desOffset+len往后挪,挪到desOffset+len处,然后在desOffset处插入目标字符串.

大体思想就是这样,是不是觉得很熟悉?? ヽ( ̄▽ ̄)ノ

下面这个方法是上面思路的具体实现,详细的逻辑分析已经放到代码注释中.

//插入字符串,从dstOffset索引处开始插入,插入内容为s中的[start,end]字符串

public AbstractStringBuilder insert(int dstOffset, CharSequence s,

int start, int end) {

//1. 空处理

if (s == null)

s = “null”;

//2. 越界判断

if ((dstOffset < 0) || (dstOffset > this.length()))

throw new IndexOutOfBoundsException("dstOffset "+dstOffset);

//3. 入参检测是否合法

if ((start < 0) || (end < 0) || (start > end) || (end > s.length()))

throw new IndexOutOfBoundsException(

"start " + start + ", end " + end + ", s.length() "

  • s.length());

//4. 长度记录

int len = end - start;

//5. 判断一下 当前数组长度+需要添加的字符串长度 是否够装,不够装就扩容(扩容时还有复制原内容到新数组中)

ensureCapacityInternal(count + len);

//6. 将原数组中dstOffset开始的count - dstOffset个字符复制到dstOffset + len处,这里其实就是腾出一个len长度的区间,用用户存放目标字符串,这个区间就是dstOffset到dstOffset + len

System.arraycopy(value, dstOffset, value, dstOffset + len,

count - dstOffset);

//7. 存放目标字符串

for (int i=start; i<end; i++)

value[dstOffset++] = s.charAt(i);

//8. 记录字符串长度

count += len;

//9. 返回自身引用 方便链式调用

return this;

}

4.删除

源码里面的删除操作实际上是复制,比如下面这个方法删除start到end之间的字符,实际是将以end开始的字符复制到start处,并且将数组的长度记录count减去len个

//删除从start到end索引区间( [start,end)前闭后开区间 )内内容

public AbstractStringBuilder delete(int start, int end) {

if (start < 0)

throw new StringIndexOutOfBoundsException(start);

if (end > count)

end = count;

if (start > end)

throw new StringIndexOutOfBoundsException();

int len = end - start;

//当start==end时不会改变

if (len > 0) {

//将value数组的start+len位置开始的count-end个字符复制到value数组的start位置处. 注意,并且将数组count减去len个.

System.arraycopy(value, start+len, value, start, count-end);

count -= len;

}

return this;

}

5.切割

我擦,原来StringBuffer的切割效率并不高嘛,其实就是new了一个String….

public String substring(int start, int end) {

if (start < 0)

throw new StringIndexOutOfBoundsException(start);

if (end > count)

throw new StringIndexOutOfBoundsException(end);

if (start > end)

throw new StringIndexOutOfBoundsException(end - start);

return new String(value, start, end - start);

}

6.改

改其实就是对其替换,而在源码中替换最终的实现其实是复制(还是复制…( ̄▽ ̄)~*).

大体思路: 假设需要将字符串str替换value数组中的start-end中,这时只需将end后面的字符往后移动,在中间腾出一个坑,用于存放需要替换的str字符串.最后将str放到value数组中start索引处.

public AbstractStringBuilder replace(int start, int end, String str) {

//1. 入参检测合法性

if (start < 0)

throw new StringIndexOutOfBoundsException(start);

if (start > count)

throw new StringIndexOutOfBoundsException(“start > length()”);

if (start > end)

throw new StringIndexOutOfBoundsException(“start > end”);

if (end > count)

end = count;

//2. 目标String长度

int len = str.length();

//3. 计算新的数组的长度

int newCount = count + len - (end - start);

//4. 判断一下是否需要扩容

ensureCapacityInternal(newCount);

//5. 将value数组的end位置开始的count - end个字符复制到value数组的start+len处. 相当于把end之后的字符移到最后去,然后中间留个坑,用来存放str(需要替换成的值)

System.arraycopy(value, end, value, start + len, count - end);

//6. 这是String的一个方法,用于将str复制到value中start处 其最底层实现是native方法(getCharsNoCheck() )

str.getChars(value, start);

//7. 更新count

count = newCount;

return this;

}

7.查询

查询是最简单的,就是返回数组中相应索引处的值.

public char charAt(int index) {

if ((index < 0) || (index >= count))

throw new StringIndexOutOfBoundsException(index);

return value[index];

}

四、StringBuffer源码分析


定义

public final class StringBuffer

extends AbstractStringBuilder

implements java.io.Serializable, CharSequence

StringBuffer和StringBuilder都是相同的继承结构.都是继承了AbstractStringBuilder.

StringBuffer和StringBuilder构造方法,可以看到默认大小是16,

public StringBuffer() {

super(16);

}

public StringBuffer(int capacity) {

super(capacity);

}

1. 我们先来看看StringBuffer的append方法

啥,不就是调用父类的append方法嘛…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是,请 注意:前面说了StringBuffer是线程安全的,为什么,源码里面使用了synchronized给方法加锁了.

public synchronized StringBuffer append(boolean b) {

toStringCache = null;

super.append(b);

return this;

}

@Override

public synchronized StringBuffer append(char c) {

toStringCache = null;

super.append©;

return this;

}

@Override

public synchronized StringBuffer append(int i) {

toStringCache = null;

super.append(i);

return this;

}

2.StringBuffer的其他方法

几乎都是所有方法都加了synchronized,几乎都是调用的父类的方法.

public synchronized StringBuffer delete(int start, int end) {

toStringCache = null;

super.delete(start, end);

return this;

}

public synchronized StringBuffer replace(int start, int end, String str) {

toStringCache = null;

super.replace(start, end, str);

return this;

}

public synchronized int indexOf(String str, int fromIndex) {

return super.indexOf(str, fromIndex);

}

五、StringBuilder分析


定义

public final class StringBuilder

extends AbstractStringBuilder

implements java.io.Serializable, CharSequence

1. 我们先来看看StringBuilder的append方法

啥,还是调用父类的append方法嘛…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是,请 注意:前面说了StringBuilder不是线程安全的,为什么,源码里面没有使用synchronized进行加锁.

public StringBuilder append(boolean b) {

super.append(b);

return this;

}

@Override

public StringBuilder append(char c) {

super.append©;

return this;

}

@Override

public StringBuilder append(int i) {

super.append(i);

return this;

}

2.StringBuilder的其他方法

最后

Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。

人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。

资源持续更新中,欢迎大家一起学习和探讨。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

chronized StringBuffer replace(int start, int end, String str) {

toStringCache = null;

super.replace(start, end, str);

return this;

}

public synchronized int indexOf(String str, int fromIndex) {

return super.indexOf(str, fromIndex);

}

五、StringBuilder分析


定义

public final class StringBuilder

extends AbstractStringBuilder

implements java.io.Serializable, CharSequence

1. 我们先来看看StringBuilder的append方法

啥,还是调用父类的append方法嘛…

[外链图片转存中…(img-NL8tZVEz-1715794421233)]

但是,请 注意:前面说了StringBuilder不是线程安全的,为什么,源码里面没有使用synchronized进行加锁.

public StringBuilder append(boolean b) {

super.append(b);

return this;

}

@Override

public StringBuilder append(char c) {

super.append©;

return this;

}

@Override

public StringBuilder append(int i) {

super.append(i);

return this;

}

2.StringBuilder的其他方法

最后

Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。

人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。

资源持续更新中,欢迎大家一起学习和探讨。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 28
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值