首先说一下,String不是基本数据类型(boolean、byte、char、short、int、long、float、double),它是引用类型,那么就是说String引用的变量会指向堆内存中的某一个对象。
String位于java.lang下的一个子包,因为lang包是自动导入的,所以在使用String的时候,不需要我们手动导入这个包
String类是不可被继承的,因为源码中
其中底层还维护了一个final数组,所以String一旦创建了,就不能被改变!我们创建String对象的时候就是往这个数组添加值!
private final char value[];
首先我们了解一下创建String类型的方法
// 第一种方法,通过new关键字来创建对象
String s1 = new String("Hello");
// 第二种方法,通过直接赋值的方式来创建
String s2 = "Hello";
其中第一种方法有很多的重载方法,感兴趣的小伙伴可以点进源码中查看。
那么这两种创建方法有什么不同呢?因为String被final修饰,那么String创建的为常量,常量不是放在堆中的,而是放在运行时常量池中的,当我们想要创建一个新的字符串的时候,JVM会首先在常量池中检查是否存在这个字符串,如果存在,就直接使用常量池里面的数据,不再新建字符串,这也是为了节省空间!不存在的时候再添加进常量池中
那么我们第二种方法创建的对象是直接引用到我们常量池中的字符串,而第一种方法创建的对象是指向堆空间的一个对象,堆空间的对象再指向常量池中的字符串!说得有点抽象,画张图来解释一下
那么这张图说明我们栈中的两个变量指向的不是同一个对象,我们可以在代码输出中证明这一点
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = "Hello";
System.out.println(s1 == s2);
}
那么用==判断两个变量的内存地址是否相等,如果不相等就证明他们不是指向同一个对象
false // 结果判断为false
证明字符串是不可变的
那么为了证明字符串是不可变的,我们创建一个String变量,并打印它的内存地址
String s2 = "Hello";
System.out.println(s2.hashCode());
再对它进行修改操作,并再次打印它的内存地址
s2 = "Hello World";
System.out.println(s2.hashCode());
我们通过控制台输出来查看修改前后是否为同一个对象
69609650
-862545276 // 两次输出的哈希值不一样,那么它们就不是同一个对象
可以看到,它们的hashcode都不相同了,不是同一个对象了,因为String是不可变的,当你修改了变量后,那么将会在字符串常量池中新建一个对象出来!所以再来进行对比就不一样了。
StringBuilder与StringBuffer
因为String维护的是一个final数组,是不可变的,当我们相对字符串进行修改的时候,将会新建一个新的字符串,这将会浪费很大的内存空间,而StringBuilder与StringBuffer就不一样了,我们来看一下它们的源码
// 调用了父类的构造方法,点进去
public StringBuilder() {
super(16);
}
底层创建了一个char数组,而这个value变量
我们可以看到,它并不是用final修改的,那么说明它是可以被修改的!而StringBuffer也是一样,因为它们都继承了同一个父类
那么同理StringBuffer的底层也是采用父类的构造方法来创建数组的,StringBuffer维护的数组也是可以被修改的,但是StringBuffer是线程安全的,我们可以从底层代码看到
底层方法都是添加了synchronized同步锁的,所以它在多线程情况下是安全的,不会出现并发修改的状况,但是这样效率很低!当然这里不是重点,我们来看一下这两个类底层是怎么对数组进行修改的,定位到append方法
还是调用父类AbstractStringBuilder的方法,点进去
public AbstractStringBuilder append(String str) {
// 首先判断追加的字符串是否为空
if (str == null)
return appendNull();
// 获取到要追加字符串的长度
int len = str.length();
// 调用下面的方法,其中这个count变量就是当前字符串的长度
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
// 底层调用数组copy的方法
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
那么通过源码发现,StringBuilder底层是通过数组拷贝的方式对字符串进行追加的,那么同样StringBuffer也是一样。
小结:StringBuilder与StringBuffer的区别
-
StringBuilder与StringBuffer的功能和方法是等价的,因为它们都继承了相同的父类
-
StringBuiler是线程不安全的,而StringBuffer是线程安全的,在并发情况下使用StringBuffer可以防止并发修改
-
StringBuiler的效率比StringBuffer要快,因为StringBuffer需要添加同步锁,在并发情况下只允许一个线程对变量进行修改
String中常用的方法
-
length(),获取字符串的长度
public static void main(String[] args) {
String s = "Hello World";
System.out.println(s.length()); // 输出11
}
注意!空格也算一个字符!
-
isEmpty(),判断字符串是否为空,它返回一个boolean类型
String s1 = "Hello World";
String s2 = "";
System.out.println(s1.isEmpty()); // false
System.out.println(s2.isEmpty()); // true
- charAt(int index),此方法需要一个int类型的参数,该方法返回在数组下标为index的字符
String s1 = "Hello World";
char c = s1.charAt(1); // e
System.out.println(c);
- subString(),该方法有一个重载方法,首先如果传递一个int参数,即代表从n开始截取到最后一个字符,其中包含第n个字符
String s1 = "Hello World";
String substring = s1.substring(6); // 从下标为6开始截取到最后,即从w开始截取
System.out.println(substring); // World
那么传递两个int参数(beginIndex,endIndex)就是截取一个区间,那么这个区间是一个闭区间
String s1 = "Hello World";
String substring = s1.substring(6,8); // Wo
System.out.println(substring);
注意:传递进来的参数不能超过数组的长度,并且beginIndex < endIndex
- contains(String str),判断str字符串是否在此字符串内,该方法返回一个boolean类型的变量
String s1 = "Hello World";
boolean world = s1.contains("World"); // true
System.out.println(world);
- indexof(String str),返回字符串str在调用方法字符串的位置,可以选择一个区间,该方法返回一个int类型的变量
String s1 = "Hello World";
int index = s1.indexOf("e");
System.out.println(index); // 1,因为e出现在s1数组下标为1的位置上
- toLowerCase(),将目标字符串全部转换为小写,toUpperCase(),将目标字符串全部转换为大写字母
String s1 = "Hello World";
System.out.println(s1.toLowerCase()); // hello world
System.out.println(s1.toUpperCase()); // HELLO WORLD
- trim(),将目标字符串的前后空格去除掉
String s1 = " Hello World ";
System.out.println(s1.trim()); // Hello World 注意中间的空格不会被去除!
- compareTo(String str),比较目标字符串第一个字符的Ascii码差值,如果第一个字符的ASCII码一样,那么比较第二个字符的ASCII码大小
String s1 = "Hello World";
int abc = s1.compareTo("Abc");
System.out.println(abc); // 7,因为H的ASCII码为72,而A的ASCII码为65,所以72-65 = 7
// ---------------------------------------------------------------------------
String s1 = "Hello World";
int abc = s1.compareTo("Hbc"); // 3,因为e的ASCII码为101,而b的ASCII码为98,所以101-98 = 3
System.out.println(abc);
- split(String regex),以regex为规则对目标字符串的分割,它返回一个数组
String s1 = "Hello World";
String[] s = s1.split(" "); // 以空格形式分割,那么分割出来即s[0] = Hello,s[1] = World
for (String s2 : s) {
System.out.println(s2); // Hello World
}
- subSequence(int beginIndex, int endIndex),返回区间在[beginIndex,endIndex]的所有字符,返回值类型为一个CharSequence
String s1 = "Hello World";
int abc = s1.compareTo("Hbc");
CharSequence charSequence = s1.subSequence(0, 3); // 返回下标为0-3的字符
System.out.println(charSequence.toString()); // Hel
- replace(CharSequence target,CharSequence replacement),将目标替换成replacement中的元素,这里我们可以组合上面学过的方法使用
String s1 = "Hello World";
String s2 = "Spring";
String replace = s1.replace(s1.subSequence(6, s1.length()), s2.subSequence(0, s2.length()));
System.out.println(replace); // Hello Spring
- equalsInoreCase(String str),与str目标字符串进行equals比较比较,忽略大小写,此方法返回一个boolean类型
String s1 = "HELLO WORLD";
String s2 = "hello world";
boolean b = s1.equalsIgnoreCase(s2);
System.out.println(b); // true
- startWith(String prefix),判断目标字符串是否以prefix开头,endsWith(String suffix),判断目标字符串是否以suffix结尾,两个方法均返回一个boolean类型的变量
String s1 = "hello world";
System.out.println(s1.startsWith("hello")); // true
System.out.println(s1.endsWith("ld")); // true
String中的一些重要的方法就介绍到这里了,在以后的编码中需要经常用到,小伙伴们想继续研究里面的方法,可以使用对象.的方式去查看里面的方法,也可以点进String源码中查看里面的所有方法
点击上面的方法名称就可以跳转到相应的方法源码中!
初次编写基础的博客,如果上述中有错误,请指出谢谢!!!!!