Java基础提升5
今天的内容是关于Java字符串的。字符串,每一个Java开发人员都会用到,但是真的对它熟悉吗?
Java中的String字符串
- Java中的String并不是基础数据类型,而是对象,当然这是最简单的认识。其次,还应该清楚 String 是被 final 关键字修饰的,也就是说 String 对象不可变,一旦对象被创建后,对象的内容是不被允许的修改的,如果如果,则会创建一个新的 String 对象,在栈中存在的变量将会指向新创建的对象,之前创建的对象有可能被垃圾回收器回收掉。
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc";
s1 = "sdf";
}
}
上述代码中,将会在常量池中开辟两块存储区域,s1最终会指向"sdf","abc"将没有任何引用指向它。最终会被回收器回收。
- String实际上是使用 数组 来存储数据的,JDK8与JDK11,数组的定义不同,但是从源码可以清晰的看到定义的数组类型。
JDK11源码
- Sting类中,一旦涉及到修改String值,就会创建一个新的String对象,并返回这个新创的对象。
String对象的创建
public class StringDemo {
public static void main(String[] args) {
String s2 = "abc";
String s1 = new String("abc");
}
}
上述代码,很多人都会用到,但是它们的差别真的清楚吗?
存储示意图
- String s1 = new String("abc"); 首先会在堆内存申请一块内存存储字符串 abc,s1指向其内存块对象。同时还会检查字符串常量池中是否含有 abc 字符串,若没有则添加abc到常量池中。所以 new String 可能会创建两个对象。
- String s2 = "abc"; 先检查字符串常量池是否含有 abc 字符串,如果有则直接指向,没有则在字符常量池添加 abc 字符串并指向它,所以这种方法最多创建一个对象,有可能不创建对象。
所以结论:
String s2 = "abc"; 最多创建一个String对象,最少不创建String对象。如果常量池中,存在”abc”,那么s2直接引用,此时不创建String对象。否则,先在常量池先创建”abc”内存空间,再引用。
String s1 = new String("abc"); 最多创建两个String对象,至少创建一个String对象。new关键字绝对会在堆空间创建一块新的内存区域,所以至少创建一个String对象。
匹配相等
使用String类经常需要对两个字符串进行对比,看是否相等。这是又有==和equals两种选择,这两者方法区别很大,可能我们会弄错,下面我们对这两种方法进行详解。
首先要明白这两种方法的用途:
- 比较类中的数值是否相等使用equals(),前提是这个类重写了equals()方法,否则equals()方法内部依旧使用==实现,比较两个包装类的引用是否指向同一个对象时使用==。
- equals()是看数值是否相等(前提是这个类重写了equals()方法),比较好理解。而==是看是否属于同一个对象。下面来举例说明==的使用。
- 先明白这个概念:常量池在Java用于保存在编译期已确定的,已编译的class文件中的一份数据。主要看编译期字符串能否确定。
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
===============
结果
===============
false
true
上述代码阐明了==与equals()的用法。下面分析几个场景:
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2);
}
}
===============
结果
===============
false
明显不是同一个对象,一个指向字符串常量池,一个指向new出来的堆内存块,new的字符串在编译期是无法确定的。所以输出false。
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc1";
String s2 = "abc" + 1;
System.out.println(s1 == s2);
}
}
===============
结果
===============
true
编译期s1和s2都是可以确定的,字符串都是 "abc1",所以s1和s2都指向字符串常量池里的 "abc1"。指向同一个对象,所以为true。
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc1";
int tmp = 1;
String s2 = "abc" + tmp;
System.out.println(s1 == s2);
}
}
===============
结果
===============
false
主要看s1和s2能否在编译期确定,s1是确定的,放进并指向常量池,而s2含有变量导致不确定,所以不是同一个对象。输出false。
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc1";
final int tmp = 1;
String s2 = "abc" + tmp;
System.out.println(s1 == s2);
}
}
===============
结果
===============
true
s1确定,加上final后使得s2也在编译期能够确定,所以输出true。
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc1";
final int tmp = getTmp();
String s2 = "abc" + tmp;
System.out.println(s1 == s2);
}
public static int getTmp() {
return 1;
}
}
===============
结果
===============
false
s1一样是确定的。而s2不能确定,需要运行代码获得tmp,所以不是同一个对象,输出false。
String的insert()方法
前面已经介绍常量池在Java用于保存在编译期已确定的,已编译的class文件中的一份数据。但我们可以通过intern()方法扩展常量池。intern()是扩充常量池的一个方法,当一个String实例str调用intern()方法时,Java会检查常量池中是否有相同的字符串,如果有则返回其引用,如果没有则在常量池中增加一个str字符串并返回它的引用。
public class StringDemo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2);
System.out.println("--------------");
s2 = s1.intern();
System.out.println(s1 == s2);
}
}
===============
结果
===============
false
--------------
true
知识点
- 单独使用""引号创建的字符串都是直接量,编译期就已经确定存储到常量池中。
- 使用new String("")创建的对象会存储到堆内存中,是运行期才创建。
- 使用只包含直接量的字符串连接符如"aa" + "bb"创建的也是直接量编译期就能确定,已经确定存储到常量池中(str2和str3)。
- 使用包含String直接量(无final修饰符)的字符串表达式(如"aa" + s1)创建的对象是运行期才创建的,存储在堆中。
- 通过变量/调用方法去连接字符串,都只能在运行时期才能确定变量的值和方法的返回值,不存在编译优化操作。
final修饰的类使用方式
Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。
- final类不能被继承、没有子类、final类中的方法默认是final的。
- final方法不能被子类的方法覆盖,但是可以被继承。
- final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
- final不能用于修饰构造函数。
注意,父类的private成员方法不能被子类方法覆盖,因此private类型的方法默认是final类型的。
- final类
final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类的时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会再被扩展,那么就可以设计成final类。 - final方法
如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明成final方法。使用final方法的原因有二:
- 把方法锁定,防止任何继承类修改它的意义与实现
- 高效,编译器在遇到调用final方法时候会转入内嵌机制,大大提高效率。
public class Test1 {
public void f1() {
System.out.println("f1");
}
public final void f2() {
System.out.println("f2");
}
public void f3() {
System.out.println("f3");
}
private void f4() {
System.out.println("f4");
}
}
public class Test2 extends Test1 {
@Override
public void f1() {
System.out.println("Test1父类方法f1被覆盖");
}
public static void main(String[] args) {
Test2 t = new Test2();
t.f1();
t.f2(); // 调用从父类继承过来的final方法
t.f3(); // 调用从父类继承过来的方法
// t.f4(); //调用失败,无法从父类继承获得
}
}
===============
结果
===============
Test1父类方法f1被覆盖
f2
f3
- final变量(常量)
用final修饰的成员变量表示常量,值一旦给定就无法修改;final修饰的变量有三种,静态变量,实例变量,局部变量,分别表示三种类型的常量。从下面的例子中可以看出,一旦给final变量初值后,值就不能再改变了。
/**
* @ClassName: Test3
* @Description: TODO
* @Author: kevin
* @Date: 2019-03-24 22:07
* @Version: 1.0
**/
public class Test3 {
private final String S = "final实例变量S";
private final int A = 100;
public final int B = 90;
public static final int C = 80;
private static final int D = 70;
public final int E; // final空白,必须在初始化对象的时候赋值
public Test3(int x) {
E = x;
}
public static void main(String[] args) {
Test3 t = new Test3(2);
// t.A=101; //出错,final变量的值一旦给定就无法改变
// t.B=91; //出错,final变量的值一旦给定就无法改变
// t.C=81; //出错,final变量的值一旦给定就无法改变
// t.D=71; //出错,final变量的值一旦给定就无法改变
System.out.println(t.A);
System.out.println(t.B);
System.out.println(t.C); //不推荐用对象方式访问静态字段
System.out.println(t.D); //不推荐用对象方式访问静态字段
System.out.println(Test3.C);
System.out.println(Test3.D);
// System.out.println(Test3.E); //出错,因为E为final空白,依据不同对象值有所不同.
System.out.println(t.E);
Test3 t1 = new Test3(3);
System.out.println(t1.E); //final空白变量E依据对象的不同而不同
}
private void test() {
System.out.println(new Test3(1).A);
System.out.println(Test3.C);
System.out.println(Test3.D);
}
public void test2() {
final int a; //final空白,在需要的时候才赋值
final int b = 4; //局部常量--final用于局部变量的情形
final int c; //final空白,一直没有给赋值.
a = 3;
// a = 4; //出错,已经给赋过值了.
// b = 2; //出错,已经给赋过值了.
}
}
另外,final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。
-
final参数
当函数参数为final类型时,可以读取使用该参数,但是无法改变参数的值。
错误案例
public class Test4 {
public static void main(String[] args) {
new Test4().f1(2);
}
public void f1(final int i) {
// i++; // i 是final类型的,值是不允许改变的
System.out.println(i);
}
}
参考链接
Java的String详解
Java学习笔记(3)—— String类详解
final修饰的类使用方式