前言

最近花了两天时间,整理了一下String的源码。这个整理并不全面但是也涵盖了大部分Spring源码中的方法。后续如果有时间还会将剩余的未整理的方法更新到这篇文章中。方便以后的复习和面试使用。如果文章中有地方有问题还请指出。

简述

字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了String 类来创建和操作字符串。字符串缓冲区支持可变字符串。因为String对象是不可变的,因此可以共享它们。

String类代表字符串,Java程序中的所有字符串字面值如"abc"都是这个类的实例对象。String 类是不可改变的,所以你一旦创建了 String 对象,那它的值就无法改变了。如果需要对字符串做很多修改,那么应该选择使用StringBuilder或者StringBuffer。

最简单的创建字符串的方式:String qc = "qiu chan"编译器会使用该值创建一个 对象。我们也可以使用关键字New创建String对象。
String类型的常量池比较特殊。它的主要使用方法有两种:
直接使用双引号声明出来的String对象会直接存储在常量池中。
如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

继承/实现关系

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 省略
}

  

String是final修饰的不能够被继承和修改。

源码

String的底层使用的是char数组用于存储。

private final char value[];

  

缓存字符串的哈希码默认值为0

private int hash;

  

无参数构造函数

public String() {
this.value = "".value;
}

  

解析:初始化一个新创建的String对象,使其代表一个空字符序列。 注意,由于String是不可变的,所以不需要使用这个构造函数。

参数为字符串的构造函数

public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

  

解析:初始化一个新创建的String对象,使其代表与参数相同的字符序列。换句话说,新创建的字符串是参数字符串的副本。除非需要参数字符串的显式拷贝,否则不需要使用这个构造函数,因为String是不可变的。

参数为char数组的构造函数

public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}

  

解析:分配一个新的String,使其代表当前字符数组参数中包含的字符序列。使用Arrays.copyOf方法进行字符数组的内容被复制。字符数组的后续修改不会影响新创建的字符串。

参数为char数组并且带有偏移量的构造方法

// value[]:作为字符源的数组,offset:偏移量、下标从0开始并且包括offset,count:从数组中取到的元素的个数。
public String(char value[], int offset, int count) {
// 如果偏移量小于0抛出IndexOutOfBoundsException异常
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
// 判断要取的元素的个数是否小于等于0
if (count <= 0) {
// 要取的元素的个数小于0,抛出IndexOutOfBoundsException异常
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// 在要取的元素的个数等于0的情况下,判断偏移量是否小于等于数组的长度
if (offset <= value.length) {
// 偏移量小于等于数组的长度,返回一个空字符串数组的形式
this.value = "".value;
return;
}
}
// 如果偏移量的值大于数组的长度减去取元素的个数抛出IndexOutOfBoundsException异常
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 复制元素
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

  

解析:分配一个新的Sting,来源于给定的char数组中的字符。offset参数是子数组中第一个字符的索引,count参数指定子数组的长度。子数组被被复制以后,对字符数组的修改不会影响新创建的字符串。

参数为StringBuffer的构造方法

public String(StringBuffer buffer) {
// 这里对StringBuffer进行了加锁,然后再进行拷贝操作。这里对其进行加锁正是为了保证在多线程环境下只能有一个线程去操作StringBuffer对象。
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}

  

解析:分配一个新的字符串,该字符串包含当前字符串缓冲区参数中包含的字符序列。Arrays.copyOf方法进行字符串缓冲区中内容的复制。这里对StringBuffer进行了加锁,然后再进行拷贝操作。这里对其进行加锁正是为了保证在多线程环境下只能有一个线程去操作StringBuffer对象。

参数为StringBuilder的构造方法

public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

  

解析:参数是StringBuilder,这个是线程不安全的,但是性能相对于StringBuffer有很大的提升,源码的注释中说通过toString方法从字符串构建器中获取字符串可能会运行得更快,通常是首选。

length方法

public boolean isEmpty() {
// 底层的char数组的长度是否为0进行判断
return value.length == 0;
} //举例
@Test
public void test_string_isEmpty(){
System.out.println(" ".isEmpty());// false
System.out.println("".isEmpty());// true
}

  

解析:返回此字符串的长度。查看源码发现,这个value是一个char数组,本质获取的是字符串对应的char数组的长度。

isEmpty方法

public boolean isEmpty() {
// 底层的char数组的长度是否为0进行判断
return value.length == 0;
} //举例
@Test
public void test_string_isEmpty(){
System.out.println(" ".isEmpty());// false
System.out.println("".isEmpty());// true
}

  

解析:判断给定的字符串是否为空,底层实现是根据char数组的长度是否为0进行判断。

charAt方法

public char charAt(int index) {
// 给定的索引小于0或者给定的索引大于这个字符串对应的char数组的长度抛出角标越界异常
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
// 获取当前的指定位置的char字符
return value[index];
}

  

解析:根据给定的索引获取当前的指定位置的char字符。如果给定的索引否小于0,或者给定的索引是大于这个字符串对应的char数组的长度抛出角标越界异常。index是从0开始到length-1结束。序列的第一个char值在索引0处,下一个在索引1处,依此类推,与数组索引一样。

getChars方法

// srcBegin:要复制的字符串中第一个字符的索引【包含】。srcEnd:要复制的字符串中最后一个字符之后的索引【不包含】。dst[]:目标数组。dstBegin:目标数组中的起始偏移量。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
// 校验起始索引小于0抛出角标越界异常
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
// 校验结束索引大于原始字符串的长度抛出角标越界异常
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
// 校验结束索引大于起始索引抛出角标越界异常
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
// 数组的拷贝
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
} // 案例
@Test
public void test_string_codePointAt(){
// 原始字符串
String h = "ahelloworld";
// 目标char数组
char[] data = new char[4];
// 执行拷贝
h.getChars(2, 6, data, 0);
System.out.println(data);
}

  

解析:将字符串中的字符复制到目标字符数组中。索引包含srcBegin,不包含srcEnd。

equals方法

// anObject:与此String进行比较的对象。
public boolean equals(Object anObject) {
// 引用相同直接返回true
if (this == anObject) {
return true;
}
// 判断给定的对象是否是String类型的
if (anObject instanceof String) {
// 给定的对象是字符串类型的转换为字符串类型
String anotherString = (String)anObject;
// 获取当前字符串的长度
int n = value.length;
// 判断给定字符串的长度是否等于当前字符串的长度
if (n == anotherString.value.length) {
// v1[]代表当前字符串对应的char数组
char v1[] = value;
// v2[]代表给定的字符串对应的char数组
char v2[] = anotherString.value;
// 遍历原始char数组,并且与给定的字符串对应的数组进行比较
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
// 任意一个位置上不相等返回false
return false;
i++;
}
// 都相等返回true
return true;
}
}
// 不是String类型,或者长度不一致返回false
return false;
}

  

解析:这个方法重写了Object中的equals方法。方法中的将此字符串与指定对象进行比较。接下来附赠一个手写的String字符串equals方法。

手写equals方法

private boolean mineEquals(String srcObject, Object anObject){
// 比较引用是否相同
if (srcObject == anObject){
return true;
}
// 引用不相同比较内容
if (anObject instanceof String){
String ans = (String) anObject;
char[] srcChar = srcObject.toCharArray();
char[] anChar = ans.toCharArray();
int n = srcChar.length;
if (n == anChar.length){
int i = 0;
while (n-- != 0){
if (srcChar[i] != anChar[i])
return false;
i++;
}
return true;
}
}
return false;
} // 测试我们自己写的equals方法
@Test
public void test_string_mine(){
String s = new String("aaa");
// 走的是引用的比较
System.out.println(s.equals(s));// true
boolean b = mineEquals(s, s);
System.out.println(b);// true
}

  

equalsIgnoreCase方法

public boolean equalsIgnoreCase(String anotherString) {
// 引用相同返回true。引用不相同进行长度、各个位置上的char是否相同
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}

  

解析:将此字符串与另一个字符串进行比较,而忽略大小写注意事项。regionMatches方法的源码很有趣的,源码里面有一个while循环,先进行未忽略大小的判断,然后进行忽略大小的判断,在忽略大小的判断中,先进行的是大写的转换进行比较,但是可能会失败【这种字体Georgian alphabet】。所以在大写转换以后的比较失败,进行一次小写的转换比较。

startsWith方法

// 判断是否以指定的前缀开头
public boolean startsWith(String prefix) {
// 0代表从开头进行寻找
return startsWith(prefix, 0);
}

  

endsWith方法

// 判断是否以指定的前缀开头
public boolean startsWith(String prefix) {
// 0代表从开头进行寻找
return startsWith(prefix, 0);
}

  

startsWith和endsWith最终的实现方法

// prefix: 测试此字符串是否以指定的前缀开头。toffset: 从哪里开始寻找这个字符串。
public boolean startsWith(String prefix, int toffset) {
// 原始的字符串对应的char[]
char ta[] = value;
// 开始寻找的位置
int to = toffset;
// 获取指定的字符串对应的char[]
char pa[] = prefix.value;
int po = 0;
// 获取指定的字符串对应的char[]长度
int pc = prefix.value.length;
// 开始寻找的位置小于0,或者起始位置大于要查找的长度【value.length - pc】返回false。
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
// 比较给定的字符串的char[]里的每个元素是否跟原始的字符串对应的char数组的元素相同
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
// 有一个char不相同返回false
return false;
}
}
// 相同返回true
return true;
}

  

substring方法

// 返回一个字符串,该字符串是该字符串的子字符串。beginIndex开始截取的索引【包含】。
public String substring(int beginIndex) {
// 校验指定的索引,小于0抛出角标越界
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// 子字符串的长度
int subLen = value.length - beginIndex;
// 子字符串的长度小于0抛出角标越界
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 开始位置为0,返回当前字符串,不为0,创建一个新的子字符串对象并返回
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

  

解析:返回一个字符串,该字符串是该字符串的子字符串。子字符串以指定索引处的字符开头【包含】,并且扩展到该字符串的末尾。

substring方法

// 返回一个字符串,该字符串是该字符串的子字符串。beginIndex开始截取的索引【包含】。
public String substring(int beginIndex) {
// 校验指定的索引,小于0抛出角标越界
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// 子字符串的长度
int subLen = value.length - beginIndex;
// 子字符串的长度小于0抛出角标越界
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 开始位置为0,返回当前字符串,不为0,创建一个新的子字符串对象并返回
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

  

解析:返回一个字符串,该字符串是该字符串的子字符串。子字符串从指定的beginIndex开始【包含】,并且扩展到索引endIndex-1处的字符【不包含】。

concat方法

public String concat(String str) {
// 获取给定的字符串的长度
int otherLen = str.length();
// 长度为0,直接返回当前的字符串
if (otherLen == 0) {
return this;
}
// 获取当前字符串的长度
int len = value.length;
// 构建一个新的长度为len + otherLen的字符数组,并且将原始的数据放到这个数组
char buf[] = Arrays.copyOf(value, len + otherLen);
// 这个底层调用是System.arraycopy这个方法的处理是使用c语言写的
str.getChars(buf, len);
return new String(buf, true);
}

  

将指定的字符串连接到该字符串的末尾。字符串拼接。

format方法

// 使用指定的格式字符串和参数返回格式化的字符串。
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
} // 案例,这里是使用%s替换后面的如"-a-"
@Test
public void test_start(){
System.out.println(String.format("ha %s hh %s a %s h", "-a-", "-b-", "-c-"));
}

  

trim方法

public String trim() {
// 指定字符串的长度
int len = value.length;
// 定义一个开始位置的索引0
int st = 0;
// 定义一个char[] val,用于避免使用getfiled操作码,这个可以写段代码反编译一下看看
char[] val = value;
// 对于字符串的开头进行去除空格,并记录这个索引
while ((st < len) && (val[st] <= ' ')) {
st++;
}
// 对于字符串的尾部进行去除空格,也记录这个索引,这个索引就是去除尾部空格后的索引
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
// 根据上面记录的长度判断是否要截取字符串
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

  

返回一个字符串,其值就是这个字符串,并去掉任何首部和尾部的空白。

join方法

// 返回一个新的String,该字符串由给定的分隔符和要连接的元素组成。delimiter:分隔每个元素的分隔符。elements:连接在一起的元素。
public static String join(CharSequence delimiter, CharSequence... elements) {
// delimiter和elements为空抛出空指针异常,null会被拦截,""不会被拦截
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
//
StringJoiner joiner = new StringJoiner(delimiter);
// 遍历给定的要拼接的元素,拼接的元素允许为null
for (CharSequence cs: elements) {
// 执行拼接方法
joiner.add(cs);
}
return joiner.toString();
} // 拼接方法
public StringJoiner add(CharSequence newElement) {
// prepareBuilder()方法首次调用会创建StringBuilder对象,后面再调用会执行拼接分隔符
prepareBuilder().append(newElement);
return this;
} // 未进行拼接创建StringBuilder对象,已经拼接以后value != null执行拼接分隔符
private StringBuilder prepareBuilder() {
// 判断拼接的value是否为空
if (value != null) {
// 不为空执行拼接分隔符
value.append(delimiter);
} else {
// 最开始使用拼接的时候,调用这个方法创建一个空的StringBuilder对象,只调一次
value = new StringBuilder().append(prefix);
}
return value;
} // 上面是调用的这个拼接元素方法
@Override
public StringBuilder append(CharSequence s) {
// 这里啥都没处理,调用的是父类的append方法,设计模式为建造者模式
super.append(s);
return this;
} // 上面的prepareBuilder方法是拼接分隔符,这个方法是将分隔符和给定的元素拼接的方法
@Override
public AbstractStringBuilder append(CharSequence s) {
// 以下3个判断根据类型和是否为空进行区别拼接
if (s == null)
return appendNull();
if (s instanceof String)
return this.append((String)s);
if (s instanceof AbstractStringBuilder)
return this.append((AbstractStringBuilder)s);
// 拼接
return this.append(s, 0, s.length());
}

  

将给定的字符串以给定的分割符分割并返回分隔后的字符串。

replace方法

// target:要被替换的目标字符串。 replacement:替换的字符串
public String replace(CharSequence target, CharSequence replacement) {
return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}

  

解析:用指定的字符串替换这个字符串中与之匹配的每个子字符串。替换从字符串的开头到结尾,例如,在字符串 "aaa "中用 "b "替换 "aa "将导致 "ba "而不是 “ab”。

replaceAll方法

// regex:这个支持正则表达式,也可以是要被替换的目标字符串。
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

  

问题:replace和replaceAll方法的区别是啥?
replaceAll支持正则表达式。

针对char的replace方法

// oldChar:要被替换的字符,newChar:替换的字符
public String replace(char oldChar, char newChar) {
// oldChar不等于newChar
if (oldChar != newChar) {
// 当前字符串的长度
int len = value.length;
// 这个用于下面的while循环里的条件比较,val[i]中的i是从0开始的
int i = -1;
// 定义一个char[] val,用于避免使用getfiled操作码,这个可以写段代码反编译一下看看
char[] val = value; /* avoid getfield opcode */
// 这个用于记录这个i的值,并且判断是否有要替换的,这个循环有利于性能的提升
while (++i < len) {
// val[i]中的i是从0开始的
if (val[i] == oldChar) {
// 有要替换的直接跳出循环
break;
}
}
// 上面的while循环中如果有要替换的i肯定小于len,如果没有下面这个判断就不会执行
if (i < len) {
// 能进到这个循环肯定是有要替换的,创建一个长度为len的char数组
char buf[] = new char[len];
// 上面的i是记录第一个可以替换的char的索引,下面这个循环是将这个i索引前的不需要被替换的填充到buf[]数组中
for (int j = 0; j < i; j++) {
// 填充buf[]数组
buf[j] = val[j];
}
// 从可以替换的索引i开始将剩余的字符一个一个填充到 buf[]中
while (i < len) {
// 获取要被替换的字符
char c = val[i];
// 判断这个字符是否真的需要替换,c == oldChar成立就替换,否则不替换
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 返回替换后的字符串
return new String(buf, true);
}
}
// oldChar等于newChar直接返回当前字符串
return this;
}

  

案例

@Test
public void test_matches(){
String a = "adddfdefe";
System.out.println(a.replace('d', 'b'));// abbbfbefe
} 仿写replace方法参数针对char

  

仿写

// 和源码给的唯一不同的是参数传递,其他的都和源码一样,自己写一遍可以加深记忆和借鉴编程思
public String replace(String source, char oldChar, char newChar) {
char[] value = source.toCharArray();
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf);
}
}
return new String(value);
}

  

intern方法

public native String intern();

  

这是一个native方法。调用String#intern方法时,如果池中已经包含一个由equals方法确定的等于此String对象的字符串,则返回来自池的字符串。否则,将此String对象添加到池中,并返回这个String的引用。

深度分析:面试腾讯,阿里面试官都喜欢问的String源码,看完你学会了吗?的更多相关文章

  1. 一名十年Java程序员回忆阿里面试经历——揭开阿里面试的“遮羞布”

    阿里面试经历 去阿里面试可以说非常非常的偶然和戏剧性,因为本人根本没投简历,以至于阿里hr给我电话的时候我一度认为是诈骗电话.因为深圳这家公司不错我还想在这里干个两年左右再考虑考虑. 这个时候的本人已 ...

  2. 基础面试,为什么面试官总喜欢问String?

    关于 Java String,这是面试的基础,但是还有很多童鞋不能说清楚,所以本文将简单而又透彻的说明一下那个让你迷惑的 String 在 Java 中,我们有两种方式创建一个字符串 String x ...

  3. 面试官:你说你精通源码,那你知道ArrayList 源码的设计思路吗?

    Arraylist源码分析 ArrayList 我们几乎每天都会使用到,但是通常情况下我们只是知道如何去使用,至于其内部是怎么实现的我们不关心,但是有些时候面试官就喜欢问与ArrayList 的源码相 ...

  4. (转)Java中的String为什么是不可变的? -- String源码分析

    背景:被问到很基础的知识点  string  自己答的很模糊 Java中的String为什么是不可变的? -- String源码分析 ps:最好去阅读原文 Java中的String为什么是不可变的 什 ...

  5. 从谷歌官网下载android 6.0源码、编译并刷入nexus 6p手机

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/fuchaosz/article/details/52473660 1 前言 经过一周的奋战,终于从谷 ...

  6. 2021超详细的HashMap原理分析,面试官就喜欢问这个!

    一.散列表结构 散列表结构就是数组+链表的结构 二.什么是哈希? Hash也称散列.哈希,对应的英文单词Hash,基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出 这个映射的规则就是对 ...

  7. 【腾讯Bugly干货分享】微信Tinker的一切都在这里,包括源码(一)

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ecdf2d98250b4631ae034b 最近半年以来,Android热补 ...

  8. string源码分析 ——转载 http://blogs.360.cn/360cloud/2012/11/26/linux-gcc-stl-string-in-depth/

    1. 问题提出 最近在我们的项目当中,出现了两次与使用string相关的问题. 1.1. 问题1:新代码引入的Bug 前一段时间有一个老项目来一个新需求,我们新增了一些代码逻辑来处理这个新需求.测试阶 ...

  9. Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO

    距离上次发布(android高仿系列)今日头条 --新闻阅读器 (二) 相关的内容已经半个月了,最近利用空闲时间,把今日头条客户端完善了下.完善的功能一个一个全部实现后,就放整个源码.开发的进度就是按 ...

随机推荐

  1. 想买保时捷的运维李先生学Java性能之 运行时数据区域

    前言 不知道自己不知道,不知道自己知道,知道自己不知道,知道自己知道,目前处于知道自己不知道这个阶段,很痛苦啊,干了4年了运维,是一个坎.越来越发觉想要走得远,还是得扎根底.   一.运行时数据区域 ...

  2. MongoDB复制 --- MongoDB基础用法(五)

    复制 MongoDB复制是将数据同步在多个服务器的过程. 复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性. 复制还允许您从硬件故障和服务中断中恢复 ...

  3. 吐槽一下Abp的用户和租户管理模块

    1. 背景 ASP.NET Core 基于声明的访问控制到底是什么鬼? 聊到基于声明的身份认证将 身份和签发机构分离,应用程序信任签发机构,故认可签发的身份信息. -- --- --- --- Cla ...

  4. Luogu P4208 [JSOI2008]最小生成树计数

    题意 给定一个 \(n\) 个点 \(m\) 条边的图,求最小生成树的个数. \(\texttt{Data Range:}1\leq n\leq 100,1\leq m\leq 10^4\) 题解 一 ...

  5. Redis常用命令(5)——Set

    SADD 格式:SADD key member [member ...] 作用:在集合key中插入一个或多个元素.如果member已经存在,则忽略member.如果key不存在则先创建集合key. 返 ...

  6. MySQL图形界面客户端

    图形界面客户端 使用图形界面客户端操作数据库更直观.方便.下面三个客户端都能操作MySQL,各有各自的优点. 1.Navicat Premium 下载安装包下载 关注公众号[轻松学编程],然后回复[n ...

  7. mysql处理数据库事务

    数据库事务 关注公众号"轻松学编程"了解更多. 1.概念 ​ 执行批量操作时,这些操作作为一个整体,要么全部成功,要么全部失败.如银行转账,己方扣钱.对方加钱,这两个操作是一个整体 ...

  8. 3.2 表 ADT -3.3 Java Collection API 中的表

    3.2 表 ADT 处理形如 A0, A1, A2, --, AN-1 的一般的表.我们称这个表大小为N.将大小为0的特殊表称为空表 对于除空表以外的任何表,称 Ai-1 前驱 Ai,Ai 后继 Ai ...

  9. 【SpringBoot】07.SpringBoot文件上传

    SpringBoot文件上传 1.编写html文件在classpath下的static中 <!DOCTYPE html> <html> <head> <met ...

  10. python3批量修改文件后缀名

    import os # 原文件后缀名 suffix_name = '.jar.src.zip' # 新文件后缀名 nwe_suffix_name = '.jar' def foo(path1): fi ...