String杂烩

String对象是不可变的。由于string的只读特性,使得在string的操作上有一定的性能问题。比如对于string的重载操作符"+"的使用会产生一大堆的中间件对象。

String str="abc";
System.out.println(str);
str=str+"de";
System.out.println(str);
/*
如果运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,
其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,
并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,
然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)
给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。
所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,
所以执行速度很慢。而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对
该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多
*/
String str="abc"+"de";
StringBuilder stringBuilder=new StringBuilder().append("abc").append("de");
System.out.println(str);
System.out.println(stringBuilder.toString());

字符串三兄弟

String、StringBuilder、StringBuffer

我们先看一下 StringBuilder,在百度百科中它的解释为:

StringBuilder是一个可变的字符序列。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。

如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。 在 StringBuilder 上的主要操作是 append 和 insert 方法。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符添加或插入到字符串生成器中。append 方法始终将这些字符添加到生成器的末端;而 insert 方法则在指定的点添加字符。

因为它是一个可以动态增加自身数据长度的类,所以其默认长度为 16。当然它也可以扩容。

首先我们要明确 StringBuffer类与 StringBuilder类均继承了抽象类 AbstractStringBuilder类。而它们的默认长度设置则是在父类的构造方法中实现的,而它们指定长度的有参构造方法则是它们自己的。

StringBuilder 的几个常用方法

  1. append 方法可用来将文本或对象的字符串表示形式添加到由当前 StringBuilder对象表示的字符串的结尾处。

  2. appendFormat 方法将文本添加到 StringBuilder的结尾处,而且实现了 IFormattable接口,因此可接受格式化部分中描述的标准格式字符串。可以使用此方法来自定义变量的格式并将这些值追加到 StringBuilder的后面。

    int MyInt= 25;
    StringBuilder MyStringBuilder = new StringBuilder("Your total is ");
    MyStringBuilder.AppendFormat("{0:C} ", MyInt);
    System.out.println(MyStringBuilder);  //Your total is $25.00
    
  3. insert 方法将字符串或对象添加到当前 StringBuilder中的指定位置

  4. remove方法从当前 StringBuilder中移除指定数量的字符,移除过程从指定的从零开始的索引处开始

  5. replace方法,可以用另一个指定的字符来替换 StringBuilder对象内的字符

这三个类的主要区别在两个方面

  1. 运行方面

    在这方面运行速度快慢为:StringBuilder > StringBuffer > String

    String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。

  2. 线程安全

    在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的

    如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。

总结

String:适用于少量的字符串操作的情况

StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况

StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

正则化表达式

StringTokennizer

StringTokennizer是一个分解字符串的工具类,类似于Java String 类中的split函数。

StringTokenizer类的常用方法:

  1. countTokens() 统计分隔符数量

  2. hasMoreElements() , nextElement() 和hasMoreTokens(), nextToken(), nextToken(String delim) 匹配和寻找分隔符

Pattern

量词描述了一个模式吸收输入文本的方式:贪婪型、勉强型、占有型(java中才有的量词)

查看正则表达式符号

Pattern.compile() 还有一个版本:Pattern Pattern.compile(String regex, int flag)。它接收一个标记参数,以调整匹配的行为。其中的flag来自以下的Pattern类中的常量

编译标记 效果
Pattern.CANON_EQ 当且仅当两个字符的"正规分解(canonical decomposition)“都完全相同的情况下,才认定匹配。比如用了这个标志之后,表达式"a\u030A"会匹配”?"。默认情况下,不考虑"规范相等性(canonical equivalence)"。
Pattern.CASE_INSENSITIVE(?i) 默认情况下,大小写不敏感的匹配只适用于US-ASCII字符集。这个标志能让表达式忽略大小写进行匹配。要想对Unicode字符进行大小不明感的匹 配,只要将UNICODE_CASE与这个标志合起来就行了。
Pattern.COMMENTS(?x) 在这种模式下,匹配时会忽略(正则表达式里的)空格字符(不是指表达式里的"\s",而是指表达式里的空格,tab,回车之类)。注释从#开始,一直到这行结束。可以通过嵌入式的标志来启用Unix行模式。
Pattern.DOTALL(?s) 在这种模式下,表达式’.‘可以匹配任意字符,包括表示一行的结束符。默认情况下,表达式’.'不匹配行的结束符。
Pattern.MULTILINE(?m) 在这种模式下,’^‘和’KaTeX parse error: Expected group after '^' at position 19: …匹配一行的开始和结束。此外,'^̲'仍然匹配字符串的开始,''也匹配字符串的结束。默认情况下,这两个表达式仅仅匹配字符串的开始和结束。
Pattern.UNICODE_CASE(?u) 在这个模式下,如果你还启用了CASE_INSENSITIVE标志,那么它会对Unicode字符进行大小写不明感的匹配。默认情况下,大小写不敏感的匹配只适用于US-ASCII字符集。
Pattern.UNICODE_CASE(?u) 在这个模式下,只有’\n’才被认作一行的中止,并且与’.’,’^’,以及’$'进行匹配。

在这些标记中,Pattern.CASE_INSENSITIVE,Pattern.MULTILINE,Pattern.COMMENTS特别有用。

可以通过"或"操作符组合多个标记的功能

Pattern p = Pattern.compile("^java",Pattern.CASE_INSENITIVE | Pattern.MULTILINE)

Scanner:一个用于扫描输入文本的新的实用程序。它是以前的StringTokenizer和Matcher类之间的某种结合。Scanner的构造器可以接收任何类型的输入。Scanner类可以任意地对字符串和基本类型(如int和double)的数据进行分析。借助于Scanner,可以针对任何要处理的文本内容编写自定义的语法分析器。

String探索

//请解释一下一下两行代码
String s = "hello";
String s = new String("hello");

答:当 java 程序直接使用形如 “hello” 这样的字符串直接量(包括在编译时就可以计算出来的字符串值)时,JVM 将会使用常量池来管理这些字符串;当使用 new String(“hello”) 这样的表达式时,JVM 会先使用常量池来管理 “hello” 直接量,再调用 String 类的构造器来创建一个新的 String 对象,新创建的 String 对象被保存在堆内存中。换句话说,new String(“hello”) 一共产生了两个字符串对象

字符串常量池(constant pool) 专门用于管理在编译时被确定并保存在已编译的 .class 文件中的一些数据。它包括了关于类、方法、接口中的常量,还包括字符串常量

相对于其它的八种基本数据类型,String 是对象。这些类型为了使它们在运行的时候速度更快、更好地节省内存,都提供了一种常量池的概念,常量池类似于 java 系统级别的缓存。八种基本类型的常量池都是由系统协调的,而 String 类型的常量池比较特殊。它的主要使用方法有两种:

  1. 直接使用双引号声明出来的 String 对象会直接存储在常量中
  2. 如果不是用双引号声明的 String 对象,可以直接使用 String 提供的 intern() 方法,intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在会将当前字符串放入常量池
public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2); 

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}
// 输出 true	false
public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}
// 输出 false	false

在第一段代码中,先看 s3和s4字符串。String s3 = new String("1") + new String("1");,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是”11″,但此时常量池中是没有 “11”对象的。

接下来s3.intern();这一句代码,是将 s3中的"11"字符串放入String 常量池中,因为此时常量池中不存在"11"字符串,因此常规做法是在常量池中生成一个"11"的对象。

最后String s4 = "11"; 这句代码中”11″是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向s3引用对象的一个引用。所以s4引用就指向和s3一样了。因此最后的比较 s3 == s4 是 true。

再看s和 s2 对象。String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。

接下来String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。

来看第二段代码,第一段代码和第二段代码的改变就是 s3.intern(); 的顺序是放在String s4 = "11";后了。这样,首先执行String s4 = "11";声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行s3.intern();时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。

第二段代码中的 s 和 s2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。

在 jdk6 以及以前的版本中,字符串的常量池是放在 java 的 Perm 区的。但是因为 Perm 区域太小了,所以jdk7 之后便转移到了正常的 java Heap 区域了。而在 jdk8 中已经取消了 Perm 区域,而是使用做“Metaspace”的本地内存(Native memory),如果想要详细地了解的话可以查看这个博客:https://blog.csdn.net/mawming/article/details/51967291

intern 的使用

static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
 
public static void main(String[] args) throws Exception {
    Integer[] DB_DATA = new Integer[10];
    Random random = new Random(10 * 10000);
    for (int i = 0; i < DB_DATA.length; i++) {
        DB_DATA[i] = random.nextInt();
    }
    long t = System.currentTimeMillis();
    for (int i = 0; i < MAX; i++) {
        //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
         arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
    }
 
    System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();
}

不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。使用了intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String 后, 然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 JVM垃圾回收的时间是要远远大于这点时间的。

默认的 StringPool 的长度是1009且不可变的。因此一旦常量池中的字符串达到的一定的规模后,性能会急剧下降。

//场景一:
String s1 = "123" + "456";
 
//场景二:
String s1 = "123";
String s2 = s1 + "456";

在 Java 中,对于字符串连接它会优化成使用 StringBuilder 来完成连接,那么它们实际上会转变成 :

//场景二
String s1 = "123";
String s2 = new StringBuilder.append(s).append("456").toString();
/*
在这种情况,它依然涉及到 “123” 和 “456” 两个字面量,因此在常量池中仍会保存这两个字面量对象,然后再在堆中创建一个 “123456” 对象,这里涉及了三个对象的创建。
在场景一种,对于字面量的连接,Java 会将其优化成 String s1 = “123456”,最终这里只会创建一个对象。
*/
发布了12 篇原创文章 · 获赞 1 · 访问量 196

猜你喜欢

转载自blog.csdn.net/qq_42885587/article/details/104396091