数据类型:字符串String

字符串常量池

Java 中字符串对象创建有两种形式,

一种为字面量形式,如 String str = "abc";,另一种就是使用 new 这种标准的构造对象的方法,如 String str = new String("abc");,

这两种方式我们在代码编写时都经常使用,尤其是字面量的方式。然而这两种实现其实存在着一些性能和内存占用的差别。

这一切都是源于 JVM 为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池。

工作原理

当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,

否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。

public class Test {
    public static void main(String[] args) {

        String s1 = "abc";
        String s2 = "abc";

        // 以上两个局部变量都存在了常量池中
        System.out.println(s1 == s2); // true


        // new出来的对象不会放到常量池中,内存地址是不同的
        String s3 = new String();
        String s4 = new String();

        /**
         * 字符串的比较不可以使用双等号,这样会比较内存地址
         * 字符串比较应当用equals,可见String重写了equals
         */
        System.out.println(s3 == s4); // false
        System.out.println(s3.equals(s4)); // true
    }
}
View Code

字符型常量和字符串常量的区别?

形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的 0 个或若干个字符

含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)

占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两个字节),

字符封装类 Character 有一个成员常量 Character.SIZE 值为 16,单位是bits,该值除以 8(1byte=8bits)后就可以得到 2 个字节

 

装箱与拆箱

深入解析常量池与装箱拆箱

  // 普通的创建对象方式
        Integer a = new Integer(5);
        // 装箱 调用了valueOf方法
        Integer b = 5;
        // 拆箱 调用了intValue方法
        int c = b + 5;
View Code

包装类存在一个缓存机制,当数值在-128到127之间时会从缓存中获取。

// 装箱过程中,反编译回来发现调用的是valueOf方法,在一范围内会缓存起来,引用的地址相同。
        Integer a=5;
        Integer b=5;
        System.out.println(a==b);//true  比较的是引用的地址
        
        Integer c=129;
        Integer d=129;
        System.out.println(c==d);//false
View Code

String对象一经创建就不可修改。

String 对象的两种创建方式

//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,

//然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";

String str1 = "abcd";

String str2 = new String("abcd");//堆中创建一个新的对象

String str3 = new String("abcd");//堆中创建一个新的对象

System.out.println(str1==str2);//false

System.out.println(str2==str3);//false这两种不同的创建方法是有差别的。

第一种方式是在常量池中拿对象;

第二种方式是直接在堆内存空间创建一个新的对象。

记住一点:只要使用 new 方法,便需要创建新的对象

String 类型的常量池比较特殊。它的主要使用方法有两种

直接使用双引号声明出来的 String 对象会直接存储在常量池中。

如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。

String.intern() 是一个 Native 方法,它的作用是:

如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;

如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。

String s1 = new String("计算机");

String s2 = s1.intern();

String s3 = "计算机";

System.out.println(s2);//计算机

System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,

System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象

字符串拼接

尽量避免多个字符串拼接,因为这样会重新创建对象。

如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。

String str1 = "str";

String str2 = "ing";

String str3 = "str" + "ing";//常量池中的对象

String str4 = str1 + str2; //在堆上创建的新的对象

String str5 = "string";//常量池中的对象

System.out.println(str3 == str4);//false

System.out.println(str3 == str5);//true

System.out.println(str4 == str5);//false

String s1 = new String("abc");这句话创建了几个字符串对象

将创建 1 或 2 个字符串。如果池中已存在字符串文字“abc”,则池中只会创建一个字符串“s1”。

如果池中没有字符串文字“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

Srtring 的方法

final类不能被继承,不能被覆盖

Spring为final类,里面存储数据的成员也是final的。不能被继承与修改

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
View Code

这里面value是引用,引用不可变,是否可以更改引用的变量来达到改变String的目的呢?

public static void main(String[] args) {
    char[] arr = new char[]{'a', 'b', 'c', 'd'};
    String str = new String(arr);
    arr[3] = 'e';
    System.out.println("str= " + str);
    System.out.println("arr[]= " + Arrays.toString(arr));
    //str= abcd
    //arr[]= [a, b, c, e]
    //用数据atr来初始化String。然后想通过改变atr的引用的值来达到改变String的目,发现没达到目的。
}
View Code

这是因为在初始化String时,不是直接将引用进行赋值,而是将value的值重新拷贝了一份。

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

尝试采用String提供的方法,因为有不少方法看起来是可以改变String对象的,如replace()、replaceAll()、substring()

//  当使用substring时是重新new了一个对象
public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
            : new String(value, beginIndex, subLen);
}

 */
public String replace(char oldChar, char newChar) {
    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, true);//重新new了一个对象
        }
    }
    return this;
}
View Code

StringBuilder和StringBuffer的区别

参考:

https://mp.weixin.qq.com/s/ZyhIHDU7AC0bakorsha12Q

StringBuilder不是线程安全的,StringBuffer是线程安全的

简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的。

在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串 private final byte[] value

而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是AbstractStringBuilder 实现的,大家可以自行查阅源码。

AbstractStringBuilder.java

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }}
View Code

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。

StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。

相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

操作少量的数据: 适用 String

单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder

多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

 

String类为什么是final类型?

1. 为了实现字符串池

2. 为了线程安全

3. 为了实现String可以创建HashCode不可变性

只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。

如果字符串是可变的,那么会引起很严重的安全问题。因为字符串是不可变的,所以它的值是不可改变的,否则改变字符串指向的对象的值,造成安全漏洞。

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。

譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。

这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

判断字符串是否为数字

1、使用java自带的函数

public static boolean isNumeric1(String str){
          int len=str.length();
          if(len>1&&str.charAt(0)=='0'){//当大于2位时,左起第一位不能为0
              return false;
          }
          for(int i=0;i<len;i++){//左起每一位都不能为除数字外其他字符
              char iStr=str.charAt(i);
              if(!Character.isDigit(iStr)){
                   return false;
              }
          }
          return true;
     }
View Code

2、采用正则表达式

   public static boolean isNumeric1(String str){
          Pattern  pattern=Pattern.compile("^-?([0-9])|([1-9][0-9]{1,})$");
          return pattern.matcher(str).matches();
     }
View Code

 

 参考:

【Java基本功】一文读懂String及其包装类的实现原理

 

 

 

 

 

 

 

posted @ 2021-04-07 20:04  弱水三千12138  阅读(347)  评论(0)    收藏  举报