今天的博客主题
基础篇 --》常用类 --》String类
JDK常用的包
在说String类之前,介绍几个JAVA当中几个比较常用的包。
java.lang: 这个是系统的基础类,比如String、Math、Integer、System和Thread提供常用功能。
java.io: 这里面是所有输入输出有关的类,比如文件操作等。
java.net:这里面是与网络有关的类,比如URL,URLConnection等。
java.util: 这个是系统辅助类,特别是集合类Collection,List,Map等。
java.sql: 这个是数据库操作的类,Connection, Statememt,ResultSet等
Object
还得在介绍一位:Object 所有类的老祖宗
所有的类直接或者间接继承父类 Java认为所有的对象都具备一些基本的共性内容 这些内容可以不断的向上抽取 最终就抽取到了一个最顶层的类中(Object) 该类中定义的就是所有对象都具备的功能。
具体方法有:
boolean equals(Object obj): 用于比较两个对象是否相等 其实内部比较的就是两个对象地址。
String toString(): 将对象变成字符串 默认返回的格式: 类名@哈希值 = getClass().getName() + '@' + Integer.toHexString(hashCode()) 为了对象对应的字符串内容有意义 可以通过复写 建立该类对象自己特有的字符串表现形式。
Class getClass(): 获取任意对象运行时的所属字节码文件对象。
int hashCode(): 返回该对象的哈希码值 支持此方法是为了提高哈希表的性能。
通常equals, toString, hashCode在应用中都会被复写 建立具体对象的特有的内容。
String概述
String 字符串类
Java里面应用比较多,在Java里面String属于一个对象,Java提供了一系列操作字符串的属性方法。
要知道String不属于8种基本数据类型,String是一个对象。
因为对象的默认值是null,所以String的默认值也是null,但它又是一种特殊的对象,有其它对象没有的一些特性。
字符串是常量,值在创建之后不能被更改,字符串所有的字面值都作为此类的实力实现。
String源码
public final class String
implements java.io.Serializable, Comparable<java.lang.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;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
...等等提供了很多属性方法
}
通过源码可以看出String底层使用一个字符数组来维护的。
String类被final修饰了,意味着String类不能被继承,成员方法默认为final方法
创建String字符串
创建一个字符串的方式有多种
可以先声明后赋值,可以直接声明并赋值,可以通过实例化的方式来声明
例
public class StringDemo {
public static void main(String[] args) {
String str1;// 先声明
str1 = "HelloWorld";// 后赋值
System.out.println(str1);
String str2 = "你好,世界"; // 直接声明并赋值
System.out.println(str2);
//通过实例化方式
String str3 = new String("来了老弟");
System.out.println(str3);
}
}
输出结果
HelloWorld
你好,世界
来了老弟
String字符串对象内存分配机制
直接声明赋值的方式创建的字符串对象被放在方法区的常量池里面,也叫字符串常量池。
通过构造方法实例化出来的字符串对象是在堆内存中。
字符串对象的内存分配和其他对象内存分配一样,需要消耗很多时间和空间,而且字符串在Java中使用比较多。
为了提高性能减少内存开销,JVM在实例化字符串时做了优化,就是开辟了字符串常量池。
每当创建一个字符串时,JVM会先检查字符串常量池里面是不是已经存在了这个字符串,如果存在了就直接返回常量池中的实例引用。
不存在就实例化该字符串,放到常量池中。
由于字符串的不可变性,可以十分确定常量池中不会出现两个一模一样的字符串。
常量池(简单了解下,将JVM会着重讲解的)
常量池分为:静态常量池和运行时常量池
静态常量池:是*.class文件中的常量池,class文件中的常量池不仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,
如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了类和接口的全限定名,字段名称和描述符,方法名称和描述符三种类型的常量。
运行时常量池:是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,
也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
这个方法一般被称为手动放入常量池,String的intern()方法会查找在常量池中是否存在一份equals相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
常量池的好处
上边说了常量池是为了避免频繁的创建和销毁对象而影响系统性能,说白了其实就是实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
- 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
- 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
其实字符串常量池这个问题涉及到一个设计模式,叫“享元模式”,顾名思义 - - - > 共享元素模式
通过一个例题解读下==和equals区别
public class StringDemo {
public static void main(String[] args) {
String str1 = "aaaa";
String str2 = "aaaa";
System.out.println(str1 == str2); //true
System.out.println(str1.equals(str2)); //true
String str3 = new String("aaaa");
System.out.println(str1 == str3); //false
System.out.println(str1.equals(str3)); //true
String str4 = new String("aaaa").intern();
System.out.println(str1 == str4); //true
String str5 = "aa" + "aa";
System.out.println(str1 == str5); //true
System.out.println(str1.equals(str5)); //true
}
}
分析
执行String str1 = "aaaa";时会先去常量池里检查是否存在常量"aaaa",如果不存在在常量池创建这个对象,然后将新创建这个对象的引用地址返回给字符串常量str1,
str1常量就指向常量池中”aaaa“这个字符串对象。如果存在,不创建任何对象,直接池中”aaaa“这个对象地址返回,赋给str1。
执行String str2 = "aaaa";时也是先去常量池检是否存在常量“aaaa”,这时候已经存在了,就不创建了,直接返回了“aaaa”,对象的引用地址给str2。
也就是说str1和str2指向了同一个对象地址,==就是用来比较两个对象的引用地址是否相同的,因此 str1 == str2 为 true
equals是String类的一个方法,用来比较两个字符串的字面值是否一样,不管你的引用地址在哪,只要两个值一样就为 true,故 str1.equals(str2) 为 true
执行String str3 = new String("aaaa");时会开辟两块内存空间,引用对象str3存放在栈内存中,这时候首先查看常量池中是否有相同值的字符串,如果有,则拷贝一份到堆中,
然后返回堆中的地址指向str3;如果池中没有,则在堆中创建一份,然后返回堆中的地址指向str3。堆内存存放new出来的 aaaa 对象,并将堆中引用地址指向str3,刚刚说了
== 比较两个对象的引用地址是否相同的,因此 str1 == str3 为false。这个new出来的字符串对象是不会自动入常量池的,像str1那样创建出来的会自动放入常量池。
equals用来比较两个字符串的字面值是否一样,因此 str1.equals(str3) 为 true。
如果想把str3创建出来的对象放入常量池则需要手动来处理,调用String的intern()方法。看分析
执行String str4 = new String("aaaa").intern();这断代码时,String str4 = new String("aaaa")和str3一样,只不过后边调用了intern()方法,手动把str4创建出来的对象,手动的
放入常量池一份,这时候就会把常量池里的aaaa对象引用地址,赋值给str4,因此 str1 == str4 为 true。
执行String str5 = "aa" + "aa";代码时,+号之前说过是前后都是字符串时,做字符串的拼接,既然这样,那+号前后就是一个字符串常量,加起来肯定也是一个字符串常量,
所以在编辑时期就执行了字符串的拼接故 str5 = aaaa; 也就把 aaaa 放在了常量池里(不要被字面意思迷糊,这里也肯定会去执行检查动作,有就直接引用,没有在创建)
那么 str1 == str5 为 true 了。当然equals也为true,因为是比较的值。
通过以上分析总结
- == 是判断两个对象的引用地址是否相同
- equals是判断两个字符串对象值是否相同
- 字符串直接声明并赋值,JVM开辟一块内存空间,自动将这个对象保存常量池
- 通过new出来的字符串对象开辟两块内存空间,对象不会自动的保存到常量池里面,需要intern()方法才行。
一般在开发过程中不会使用实例化的方式来声明创建一个字符串。
但是会有面试题问到这个
例
String str = new String("abc");创建了几个对象?
先不说创建了几个对象,但它一定会创建对象。
一般国内大公司的面试笔试题上差不多都会出现这个题目,而网上流传的及一些书籍上都说是2个对象,这种说法比较片面。
首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建2个对象么?毫无疑问不可能,用javap -c反编译即可得到JVM执行的字节码内容:
很显然,new只调用了一次,也就是说只创建了一个对象。而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。
个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。
String常用方法
下标(索引)从0开始,长度从1开始
方法 | 功能 | 参数类型 | 返回类型 | 示范 |
substring() | 截取字符串 | int | String | String str1 = "HelloWorld"; 一个参数从下标开始,一直到字符串结束。 System.out.println(str1.substring(5)); //World 两个参数,第一个参数是开始的位置,第二个参数是结束的位置 System.out.println(str1.substring(5)); //Hello |
indexOf() | 获取指定字符在字符串中第一次出现的下标位置(区分大小写) | String | int | String str1 = "HelloWorld"; //返回字符在字符串第一次出现的下标位置 System.out.println(str1.indexOf("o")); // 4 |
lastIndexOf() | 获取指定字符在字符串中最后一次出现的下标(区分大小写) | String | int | String str1 = "HelloWorld"; //参数可以为任意字符,如果没有出现则返回-1,出现则返回字符最后一次出现的下标 System.out.println(str1.lastIndexOf("o")); // 6 |
split() | 分割字符串(区分大小写) | String | String[] | String str1 = "Hello,World,HelloWorld"; //参数可以为任意字符,返回String数组 System.out.println(str1.split(",")); // ["Hello","World","HelloWorld"] |
replace() | 替换字符串(区分大小写) | String,String | String | String str1 = "HelloWorld"; // 两个参数,第一个参数为原字符串里的字符,第二个参数为要替换的新字符 System.out.println(str1.replace("Hello","你好")); // 你好World String str1 = "aaa"; // 这种替换则会替换成ba,不会替换成ab的,请周晓。这种替换是则是从前往后进行匹配的 System.out.println(str1.replace("aa","b")); |
replaceAll() | 替换字符串(区分大小写) | String,String | String | String str1 = "Hello12World34Hello"; //支持正常的字符替换,也可以使用正则来替换。 System.out.println(str1.replaceAll("\\d+","你好")); 和replace替换区别就是replace不支持正则替换,replaceAll支持正则 |
length() | 统计字符串长度 | int | String str1 = "HelloWorld"; //无参数,直接返回字符串长度 System.out.println(str1.length()); // 10 | |
toLowerCase() | 将字符串单词大写转小写 | String | String str1 = "Hello嗯World"; //无参数,将字符串中的大写字母全转成小写字母 System.out.println(str1.toLowerCase()); // hello嗯world | |
toUpperCase() | 将字符串单词小写转大写 | String | String str1 = "Hello嗯World"; //无参数,将字符串中的小写字母全转成大写字母 System.out.println(str1.toUpperCase()); // HELLO嗯WORLD | |
contains() | 字符串是否包含某个字符 | String | boolean | String str1 = "Hello嗯World"; //判断字符串里是否包含某个字符 System.out.println(str1.contains("lo")); |
trim() | 去掉字符串两头空格 | String | String | String str1 = " Hello嗯World "; //去掉字符串两头的空格 System.out.println(str1.trim()); // Hello嗯World |
concat() | 字符串追加 | String | String | tring str1 = "HelloWorld"; //要在字符串后追加的字符 System.out.println(str1.concat("你好")); // HelloWorld你好 |
charAt() | 返回指定下标对应的字符 | int | String | String str1 = "HelloWorld"; //返回指定下标对应的字符 System.out.println(str1.charAt(5)); // W |
isEmpty() | 返回字符串是否为空字符串 | boolean | String str1 = "HelloWorld"; //返回字符串是否是一个空字符串,不能判断为null System.out.println(str1.isEmpty()); // false | |
toCharArray() | 返回字符串中每个字符元素的字符数组 | char[] | String str1 = "HelloWorld"; char[] chars = str1.toCharArray(); //返回字符串中每个字符元素的字符数组 System.out.println(chars); // [H,e,l,l,o,W,o,r,l,d] | |
endsWith() | 判断字符串是否以指定字符结尾(区分大小写) | String | boolean | String str = "HelloWorld"; //判断字符串是否以某个字符结尾 System.out.println(str.endsWith("ld")); // true |
startsWith() | 判断字符串是否以指定字符开始(区分大小写) | String | boolean | String str = "HelloWorld"; //判断字符串是否以某个字符开始 System.out.println(str.startsWith("h")); // false |
这些String方法都是比较常用的,经常使用就会记得比较牢固了。
注意
如果字符串声明创建时为null,调用String类里的方法时,会出现空指向异常。在对字符串做任何处理时一定要加非null判断。
StringBuffer StringBuilder
在Java中除了String类来处理字符串,也会使用StringBuffer和StringBuilder来处理字符串。
StringBuffer称为字符串缓冲区,它的工作原理是:预先申请一块内存,存放字符序列,如果字符序列满了,会重新改变缓存区的大小,以容纳更多的字符序列。
StringBuffer是可变对象,这个是String最大的不同。
StringBuilder用法同StringBuffer,StringBuilder和StringBuffer的区别是StringBuffer中所有的方法都是同步的,是线程安全的,但速度慢,StringBuilder的速度快,但不是线程安全的。
这两个比String处理字符串效率要高,因为StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
StringBuffer 类是可变字符串类,创建 StringBuffer 类的对象后可以随意修改字符串的内容。
每个 StringBuffer 类的对象都能够存储指定容量的字符串,如果字符串的长度超过了 StringBuffer 类对象的容量,则该对象的容量会自动扩大。
StringBuilder 类是在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
例
public class StringDemo {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("Hello,");
stringBuffer.append("World");
System.out.println(stringBuffer);
StringBuffer stringBuffer2 = new StringBuffer();
stringBuffer2.append("Hello").append("Hello");
System.out.println(stringBuffer2);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("你好,");
stringBuilder.append("世界");
System.out.println(stringBuilder);
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append("你好").append("你好");
System.out.println(stringBuilder2);
}
}
这就是简单的字符串拼接使用它们的append()方法。
这样就有人问了它和String使用+号追加字符串有啥区别呢?
public class StringDemo {
public static void main(String[] args) {
String str = "abc";
System.out.println(str);
str = str + "abc";
System.out.println(str);
}
}
你是要问这样不也是实现了字符串的追加吗?
对,是实现了字符串的追加。在效率上StringBuffer和StringBuilder更胜一筹。
分析:运行这段代码会先输出“abc”,然后又输出“abcabc”,好像是str这个对象被更改了,其实,这只是一种假象罢了.
JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,
然后再把原来的str的值和“abc”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,
也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
但是下面代码就不一样了
public class StringDemo {
public static void main(String[] args) {
String str = "abc" + "abc";
System.out.println(str);
StringBuilder stringBuilder = new StringBuilder().append("abc").append("abc");
System.out.println(stringBuilder.toString());
}
}
这一次String扳回一局了,String比StringBuilder的速度要快很多。
因为String str = "abc" + "abc"; 就可以完全看做是 String str = "abcabc"; 这样能不快吗?
线程安全问题
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的。
但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。
所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
这两个一般会比较常用在较多的字符串拼接,提高效率。一般对字符串的处理都用不到这两个类。在实际开发过程中根据需求择选使用。
同样和String一样里面提供了许多操作字符串的方法。
关于String StringBuffer和StringBuilder的面试题
String StringBuffer和StringBuilder的区别
String 字符串常量 不可变 使用字符串拼接时是不同的2个空间
StringBuffer 字符串变量 可变 线程安全 字符串拼接直接在字符串后追加
StringBuilder 字符串变量 可变 线程不安全 字符串拼接直接在字符串后追加
必知
1、StringBuilder执行效率高于StringBuffer高于String。
2、String是一个常量,是不可变的,对于每一次+=赋值都会创建一个新的对象,
3、StringBuffer和StringBuilder都是可变的,当进行字符串拼接时采用append方法,在原来的基础上进行追加,所以性能比String要高,
StringBuffer 是线程安全的而StringBuilder是线程非安全的,所以StringBuilder的效率高于StringBuffer。
4、对于大数据量的字符串的拼接,采用StringBuffer,StringBuilder。
对于String这个类的讲解就先到这。工作中和这个类打交道还是很多的。能了解到就这么多后期有发现在补充...
写的不好,如有纰漏还望大牛不吝赐教指出。