上帝视角学JAVA- 基础11-常用类01-String类相关【2021-08-10】

1、String 类

1.1 String 字符串

  • 字符串是常量,代表不可变的字符序列。

  • 常量与不可变的变量不同。虽然都是不能改变。一个是常量,一个还是变量

  • 字符串的值在创建之后就不能更改。

  • String 对象的字符内容是存储在一个字符数组 value[] 中的

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** final 的 字符数组 来存储所有字符 */
    private final char value[];
    。。。
}

String 是一个final 类,意味着不能被继承,没有子类。

实现了 Serializable 、Comparable、CharSequence 三个接口。

Serializable 表示字符串支持序列化,可以进行传输

Comparable :支持比较大小

public interface Comparable<T> {
    // 比较大小
    public int compareTo(T o);
}

CharSequence :字符序列接口

public interface CharSequence {
    // 长度
    int length();
​
    char charAt(int index);
​
    CharSequence subSequence(int start, int end);
​
    public String toString();
​
    /**
     * @since 1.8
     */
    public default IntStream chars() {
        class CharIterator implements PrimitiveIterator.OfInt {
            int cur = 0;
​
            public boolean hasNext() {
                return cur < length();
            }
​
            public int nextInt() {
                if (hasNext()) {
                    return charAt(cur++);
                } else {
                    throw new NoSuchElementException();
                }
            }
​
            @Override
            public void forEachRemaining(IntConsumer block) {
                for (; cur < length(); cur++) {
                    block.accept(charAt(cur));
                }
            }
        }
​
        return StreamSupport.intStream(() ->
                Spliterators.spliterator(
                        new CharIterator(),
                        length(),
                        Spliterator.ORDERED),
                Spliterator.SUBSIZED | Spliterator.SIZED | Spliterator.ORDERED,
                false);
    }
​
    /**
     */
    public default IntStream codePoints() {
        class CodePointIterator implements PrimitiveIterator.OfInt {
            int cur = 0;
​
            @Override
            public void forEachRemaining(IntConsumer block) {
                final int length = length();
                int i = cur;
                try {
                    while (i < length) {
                        char c1 = charAt(i++);
                        if (!Character.isHighSurrogate(c1) || i >= length) {
                            block.accept(c1);
                        } else {
                            char c2 = charAt(i);
                            if (Character.isLowSurrogate(c2)) {
                                i++;
                                block.accept(Character.toCodePoint(c1, c2));
                            } else {
                                block.accept(c1);
                            }
                        }
                    }
                } finally {
                    cur = i;
                }
            }
​
            public boolean hasNext() {
                return cur < length();
            }
​
            public int nextInt() {
                final int length = length();
​
                if (cur >= length) {
                    throw new NoSuchElementException();
                }
                char c1 = charAt(cur++);
                if (Character.isHighSurrogate(c1) && cur < length) {
                    char c2 = charAt(cur);
                    if (Character.isLowSurrogate(c2)) {
                        cur++;
                        return Character.toCodePoint(c1, c2);
                    }
                }
                return c1;
            }
        }
​
        return StreamSupport.intStream(() ->
                Spliterators.spliteratorUnknownSize(
                        new CodePointIterator(),
                        Spliterator.ORDERED),
                Spliterator.ORDERED,
                false);
    }
}

String 内部是用 字符串数组存储数据,这字符串数组也是final的。意味着每一个元素也是不可变的。

/** final 的 字符数组 来存储所有字符 */
    private final char value[];

看下面的例子:

public class StringTest {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        s1 = "hello";
        System.out.println(s1);  // hello
        System.out.println(s2);  // abc
    } 
}

String 是一个类 居然像基本数据类型一样,直接写值,而不是new出来。String的这种赋值方式叫做字面量定义

public class StringTest {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2); // true
    }
}

比较s1与s2的地址值,你会发现是同一个。

在JVM的内存结构中,字符串是保存在方法区中的字符串常量池的,而不是像其他对象一样,保存在堆空间。

当首次使用 "abc" 给s1时,常量池没有 "abc" 就会创建一个。然后将地址值给 栈空间的s1保存。

当第二次使用 "abc" 给s2时,由于常量池已经有了"abc",就不会再造一个,而是直接将地址值给s2保存。

通过字面量的方式【与new不同】给字符串赋值,此时字符串是放在字符串常量池中,常量池不会存储2个相同的字符串。

public class StringTest {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        s1 = "hello";
        System.out.println(s1);  // hello
        System.out.println(s2);  // abc
    } 
}

上面的代码执行了 s1 = "hello";

如果String是普通类,那么赋值就是通过s1保存的地址值,将这个地址里面保存的abc改为hello。而String是这样进行赋值的:当定义了"hello"时,常量池中没有hello,就创建一个hello,并把地址给s1,修改的是栈中s1保存的地址值。而不是常量池中的"abc"

因为字符串是final的,不可变。这就体现了字符串的不可变性。

 s3 = s1 + "world";

当执行上面的代码,进行字符串连接再赋值时,也是先去看看常量池有没有 "helloworld",有就将地址值给s3,没有就创建一个,将地址值给s3。 常量池中原有的 "hello" 不会变,hello的地址值也不会变。

对字符串的任何修改操作,都不会在原值上进行修改。而是看一看常量池有没有,没有就新建。

1.1.1 String对象的创建

// 字面量方式:直接赋值
String str = "hello";

// 对象方式:等价于 this.value = new char[0];
String s1 = new String();

// 对象方式: this.value = original.value;
String s2 = new String("hello");

// 对象方式:this.value = Arrays.copyof(value, value.length);
 String s3 = new String(new char[]{'h','e','l','l','o'});

// 对象方式:String(char value[], int offset, int count) 
 String s4 = new String(new char[]{'h','e','l','l','o'}, 2, 2);

对象方式 创建的String对象 都是在堆里面的。只有字面量方式赋值才是在方法区中的常量池。

看下面的代码:

public class StringTest {
    public static void main(String[] args) {
        // 方法区-常量池 存储
        String s1 = "hello";
        String s2 = "hello";
        // 堆空间存储
        String s3 = new String("hello");
        String s4 = new String("hello");
        System.out.println(s1 == s2); // true
        System.out.println(s2 == s3); // false
        System.out.println(s3 == s4); // false
    }
}

那是不是只要创建在堆空间了,就与常量池没关系?

不是的,以s3为例,s3这个对象在堆空间,但是堆空间保存的并不是hello这个字符串,而是 常量池中的 hello字符串的地址。就是说, 用new的方式最终还是用的常量池里面的东西,new方式反而多转了1到手。

因为这个原因,字面量的方式定义是效率最高的。

1.1.2 字符串拼接

public class StringTest {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = " world";

        String s3 = "hello world";
        String s4 = "hello" + " world";
        String s5 = s1 + " world";
        String s6 = "hello" + s2;
        String s7 = s1 + s2;
        System.out.println(s3 == s4); // true
        System.out.println(s3 == s5); // false
        System.out.println(s3 == s6); // false
        System.out.println(s3 == s7); // false
        System.out.println(s5 == s6); // false
        System.out.println(s5 == s7); // false
        System.out.println(s6 == s7); // false
    }
}

原因是 只要有变量参与拼接,都相当于是new 的方式创建字符串,结果在堆中。

只要常量与常量拼接结果在常量池中。

如果拼接的结果调用intern方法,返回值就在常量池中。

String s8 = s5.intern();
System.out.println(s3==s8); // true

练习:

public class StringTest {
    String str = new String("good");
    char[] ch = {'t', 'e','s','t'};

    public void change(String str, char ch[]){
        str = "test ok";
        ch[0] = 'b';
    }
    public static void main(String[] args) {
        StringTest ex = new StringTest();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str + " and "); // good and
        System.out.println(ex.ch); // best
    }
}
// ex.ch 肯定是 best。引用数据类型值传递传递的是地址值,会修改地址值里面指向的堆空间里面的值。
// 字符串是不可变量,值传递不会修改原来的值。

上面的解析你肯定不懂。哈哈

分析一下调用change的过程。

已知:成员属性str是使用new方法创建的,存储的是堆空间字符串对象的地址值,假设地址为a。堆空间字符串对象存储的是常量池中 good 的地址值假设地址为b。

如果是字面量赋值,就是直接存储的是常量池中good 的地址值。

字符数组ch我不分析,详情见前面值传递那一部分内容。

当调用change时,方法的形参str 接收 成员属性str的值。注意形参 str 与成员属性str虽然名字一样,但是不是同一个。为了区分,我带上this。 这个过程就是 String str = this.str 形参str 保存的就是this.str原来保存的地址值a。即str 保存了 地址值 a

在方法中,将"test ok"赋值给了 str,此时str变量保存的值由地址值a变成了"test ok"的地址值 假设为c

整个过程只有形参str保存的值在变。 成员属性this.str保存的东西始终没变。所以输出理所当然。

1.1.3 内存结构

 

 

上图可以看到 方法区域堆区是2个并列的结构。

堆空间还可以具体划分:

 

 

永久区又有点问题,上图是规范。但是事实上,在实际中永久区又不属于堆,永久区就是方法区。

实际上,方法区和堆一样,是各个线程共享的内存区域。用于存储

  • 类信息

  • 普通常量

  • 静态变量

  • 编译器编译后的代码等

虽然规范上是属于堆,但是还有一个名字叫做Non-Heap,目的是为了与堆区分。

永久区是方法区的一个实现。JDK1.7 版本将原本放在永久区的字符串常量池移走了,移到堆里面去了。

1.8 又拿回来到永久区,但是永久区改名了,叫元空间,方法区的体现叫元空间了

即1.7 以前,永久区就是方法区。1.7 版本 永久区与方法区不同了。1.7与1.8 之后又不同

常量池是方法区的一部分,class文件除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池。这部分内容在类加载后进入方法区的运行时常量池中存储

1.1.4 常用方法

  • length()方法:返回底层的char数组的长度即字符串的长度。

String str = "hello world";
int length = str.length();
System.out.println(length); // 11
  • charAt(int index) 方法:取指定索引处的字符

String str = "hello world";
int length = str.length();
char c = str.charAt(5);
// 输入的索引不能超过 长度-1,即最大索引。否则报错。
// "hello world"的最大索引是 10
char c1 = str.charAt(20);
  • isEmpty() 判断字符串是否为空,实质是字符数组的长度是否为0

String str = "hello world";
String s1 = "";

boolean empty = str.isEmpty();
System.out.println(empty); // false
System.out.println(s1.isEmpty()); // true
  • toLowerCase() 全部小写/ toUpperCase()全部大写

String s1 = "hello world";
String s2 = "HELLO WORLD";
String s = s1.toUpperCase();
System.out.println(s);  // HELLO WORLD
System.out.println(s1);  // hello world
System.out.println(s2.toLowerCase(Locale.ROOT));  // hello world
  • trim() 干掉开头、末尾的全部空格,中间的空格保留

String s1 = "  hello world    ";
System.out.println("==="+s1+"===");
System.out.println("==="+s1.trim()+"===");
// 输出。 加上 === 是为了看得出空格。 
===  hello world    ===
===hello world===
  • equals() 比较字符串内容相等;equalsIgnoreCase() 忽略大小写比较内容相等。

String s1 = "hello world";
String s2 = "hello world";
String s3 = "hEllo WoRld";

System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // false
System.out.println(s1.equalsIgnoreCase(s3)); // true
  • concat() 字符串拼接,+ 号可以替代这玩意,用的不多

String s1 = "hello world";
String s3 = s1.concat("xxx");
System.out.println(s3); // hello worldxxx
  • compareTo() 比较字符串大小,一个一个字符比较。实际比较的是字符对应的编码大小。相等为返回0,大于返回1,小于返回-1

    compareToIgnoreCase() 忽略大小写比较大小。

String s1 = "abc";
String s2 = "abc";
String s3 = "abd";

System.out.println(s1.compareTo(s2)); // 0
System.out.println(s1.compareTo(s3)); // -1
  • substring() 取字符串的子串。有2个重写的方法。

    substring(int beginIndex) 取指定索引到字符串末尾的所有字符作为子串 substring(int beginIndex, int endIndex) 取开始索引到结束索引之间的字符作为子串。 [左闭 右开):包含起始索引,不包含结尾索引

String s1 = "hello world";

String substring = s1.substring(2);
String substring2 = s1.substring(2,6);
System.out.println(substring); // llo world
System.out.println(substring2); // llo
  • endsWith() 以什么结尾 startsWith() 以什么开头

    startsWith() 还有1个重载方法 startsWith(String prefix, int toffset) ,可以指定从哪个位置开始判断。

String s1 = "hello world";

System.out.println(s1.endsWith("ld")); // true 
System.out.println(s1.startsWith("h"));  // false
System.out.println(s1.startsWith("ll")); // false
System.out.println(s1.startsWith("ll", 2)); // true   从索引为2开始判断,是否以ll开头
  • contains(CharSequence s) 是否包含某个字符序列,参数是CharSequence接口。String实现了这个接口。所以传入字符串就可。

String s1 = "hello world";
boolean ll = s1.contains("ll");
System.out.println(ll); // true
  • indexof(String str) 返回 输入的字符串首次出现的位置,如果没有就返回-1

    lastIndexOf(String str) 返回输入的字符串最后出现的位置,没有就返回-1

String s1 = "hello world";

int ll = s1.indexOf("ll");
System.out.println(ll); // 2
int oo = s1.indexOf("oo");
System.out.println(oo); // -1

int l = s1.lastIndexOf("l");
System.out.println(l); // 9

 

 

indexof 和 lastIndexOf 都各有4个重载方法。

各自的前2个是 可以传入 字符或者字符串。后2个方法的第一个参数也是字符或者字符串,第二个参数是指定寻找的起始位置。即从这个位置开始找。

int l1 = s1.lastIndexOf('l');  // 9
int l2 = s1.lastIndexOf("l", 5); // 3
int l3 = s1.indexOf('l');	// 2
int l4 = s1.indexOf("l", 2);	// 2
int l5 = s1.indexOf("l", 3);	// 3
int l6 = s1.indexOf("l", 4);	// 9
  • replace(CharSequence target, CharSequence replacement) 字符串替换

    replace(char oldChar, char newChar) 字符替换

String s1 = "hello world";
String replace = s1.replace("l", "aa");
System.out.println(replace); // heaaaao woraad

String s1 = "hello world";
String replace = s1.replace('l', 'a');
System.out.println(replace); // heaao worad

replaceAll(String regex, String replacement) 输入正则表达式,进行字符传替换,全部替换

replaceFirst(String regex, String replacement) 输入正则表达式,进行字符传替换,只替换第一个

String s1 = "234hello world43255";
String s = s1.replaceAll("\\d", "");
String s2 = s1.replaceFirst("\\d", "");
System.out.println(s); // hello world
System.out.println(s2); // 34hello world43255
  • matches(String regex) 输入正则表达式匹配,返回是否匹配成功。

String s1 = "234hello world43255";
String s2 = "0791-8888888";
boolean matches1 = s1.matches("\\d+");
boolean matches2 = s2.matches("0791-\\d{7,8}");
System.out.println(matches1);	// false
System.out.println(matches2);	// true
  • split(String regex) 输入正则表达式,进行切割,返回数组。

String s1 = "234hello world43255";
String s2 = "0791-8888888-444";
String s3 = "hello|world|java";
String[] hellos = s1.split("hello");
String[] split = s2.split("-");
String[] split1 = s3.split("\\|");
for (int i = 0; i < hellos.length; i++) {
System.out.println(i);
}
for (int i = 0; i < split.length; i++) {
System.out.println(i);
}
for (String s : split1) {
System.out.println(s);
}

1.1.5 与其他类型的转换

  • 与基本数据类型、包装类之间的转换。前部分内容已经讲过,此处复习。

1、String 转 基本类型、包装类

String s = "123";

// String 转 包装类 调用 包装类的valueOf方法
Integer integer = Integer.valueOf(s);
// String 转 包装类 调用 包装类的parseXXX方法
int i = Integer.parseInt(s);

2、基本数据类型、包装类 转换为 String

Integer a = 5;
int b = 5;

// 调用 String 的valueOf方法
String s = String.valueOf(a); 
String s1 = String.valueOf(b);
// 利用 +  自动类型转换
String s2 = a + "";
String s3 = b + "";
// 调用包装类的 toString 方法
String s4 = a.toString();
  • 与 char[] 之间的转换

1、String 转 char[] 数组

String s1 = "123abc";
char [] charArray = s1.toCharArray();

for (char c : charArray) {
	System.out.print(c + "\t");
}
// 输出:1	2	3	a	b	c	

2、char[] 转 String

char [] charArray = new char[]{'1', '2', '3', 'a', 'b', 'c'};
// 调用String 的构造器即可
String s = new String(charArray);
System.out.println(s); // 123abc
  • 与 byte[] 之间的转换

1、String 转 byte[] 数组。 实际上字符内部都是以byte存储的。字符串转byte就是编码的过程。

String s1 = "abc123";

// 调用 getBytes 方法
byte[] bytes = s1.getBytes();
for (byte aByte : bytes) {
	System.out.print(aByte + "\t"); 
}
// 输出: 97   98	99	49	50	51

getBytes方法有多个重载方法。

 

 

可以传入指定的字符编码名称。

String s1 = "中国";
byte[] bytes = s1.getBytes(StandardCharsets.UTF_8);
for (byte aByte : bytes) {
    System.out.print(aByte + "\t");
}
// -28	-72	-83	 -27 -101	-67
当使用UTF-8编码时,可以看到 一个汉字占用3个字节。中: -28	-72	-83	  国: -27 -101	-67

2、byte[] 数组 转 String, 这个过程也称为解码。

byte[] bytes = new byte[]{-28,-72,-83,-27, -101,-67};

// 还是调用String的构造器,也是可以指定编码
String s = new String(bytes);
System.out.println(s); // 中国
// 指定编码
String s = new String(bytes, StandardCharsets.UTF_8);

乱码问题一般就是 编解码使用的编码格式不同,统一编码格式都能解决编码问题。

2、StringBuilder 、StringBuffer 类

StringBuilder 和 StringBuffer 非常类似。均表示可变字符序列,而且提供相关功能的方法也一样。

String:不可变的字符序列,底层使用 final char[]

StringBuffer :可变的字符序列,线程安全的,效率低。底层也是char[] 但不是final

StringBuilder 【JDK5.0新增的】:可变的字符序列,线程不安全,效率高。底层也是char[] 但不是final

源码分析:

//对应String
String str = new String(); // char[] value = new char[0]; 相当于创建了长度为0的char 数组
String str1 = new String("abc"); // char[] value = new char[]{'a','b','c'}

//对于StringBuffer 
StringBuffer sb1 = new StringBuffer(); // char[] value = new char[16]; 相当于创建了长度为16的char 数组
System.out.println(str.length()); // 0   这里返回的不是 char[] 数组的长度,而是 append的字符的长度
sb1.append('a'); // value[0] = 'a'
sb1.append('b'); // value[1] = 'b'   StringBuffer可以在原来是char数组上添加,而不是新建一个

StringBuffer sb2 = new StringBuffer("abc"); // char[] value = new char["abc".length + 16]; 相当于创建了长度为 你输入的字符串长度加上16 的char 数组

问题来了:如果后面添加的数据超过了16呢?

ans:装不了,就扩容。append之前,先计算确保一下能否添加进去。如果不能添加,就new一个新的数组,容量左移1位,扩容2倍,再加上2,把原来的数据拷贝进去。还有一些特殊情况需要考虑。2倍也装不下,左移超出限制等。

因此,如果需要保存长的可变字符串,使用StringBuilder 、StringBuffer类时,new对象的时候指定容量大小,避免扩容。

2.1 常用方法

buffer与builder的常用方法差不多。以buffer为例。

  • append() 添加数据到末尾,如下图,有13个重载的方法。基本涵盖了各种类型。返回值还是一个StringBuffer。

 

 

  • delete(int start, int end) 方法:删除 指定索引内的数据。也是左闭右开区间

StringBuffer s1 = new StringBuffer("abc");
s1.append(1);
s1.append("l");
System.out.println(s1); // abc1l
s1.delete(2,4);
System.out.println(s1); // abl
  • replace(int start, int end, String str) 方法:替换 start、end也是索引,左闭右开 str 是需要替换的内容

StringBuffer s1 = new StringBuffer("abc");
s1.append(1);
s1.append("l");
s1.replace(2,4, "ddd");
System.out.println(s1); // abdddl
  • insert 方法: 有12个重载方法。插入数据。 offset为偏移量,即设置什么时候开始插入,第二个参数是需要插入的内容。

StringBuffer s1 = new StringBuffer("abc");
s1.append(1);
s1.append("l");
s1.insert(2, 'x');
System.out.println(s1); // abxc1l
  • reverse() 方法: 反转字符串

StringBuffer s1 = new StringBuffer("abc1l");
s1.reverse();
System.out.println(s1); // l1cba
  • indexOf 与 String 类一样

  • substring 与 String 类的也一样,这个方法需要返回值。并不会对当前字符串操作。

  • length 返回字符串长度

  • charAt 与String一样

  • setCharAt() 修改指定索引的字符。

2.2 三者效率

效率其实前面已经讲过。但是要注意使用场景。

StringBuilder > StringBuffer > String

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值