美文网首页
Java基础提升5

Java基础提升5

作者: 努力的土豆 | 来源:发表于2019-03-24 22:25 被阅读0次

    今天的内容是关于Java字符串的。字符串,每一个Java开发人员都会用到,但是真的对它熟悉吗?

    Java中的String字符串

    1. Java中的String并不是基础数据类型,而是对象,当然这是最简单的认识。其次,还应该清楚 String 是被 final 关键字修饰的,也就是说 String 对象不可变,一旦对象被创建后,对象的内容是不被允许的修改的,如果如果,则会创建一个新的 String 对象,在栈中存在的变量将会指向新创建的对象,之前创建的对象有可能被垃圾回收器回收掉。
    public class StringDemo {
    
        public static void main(String[] args) {
            String s1 = "abc";
            s1 = "sdf";
        }
    }
    

    上述代码中,将会在常量池中开辟两块存储区域,s1最终会指向"sdf","abc"将没有任何引用指向它。最终会被回收器回收。

    1. String实际上是使用 数组 来存储数据的,JDK8与JDK11,数组的定义不同,但是从源码可以清晰的看到定义的数组类型。
      JDK11源码
    JDK8源码
    1. 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类型的。
    1. final类
      final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类的时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会再被扩展,那么就可以设计成final类。
    2. 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
    
    1. 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数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。

    1. 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修饰的类使用方式

    相关文章

      网友评论

          本文标题:Java基础提升5

          本文链接:https://www.haomeiwen.com/subject/mehpvqtx.html