初阶数据结构(11)(字符串常量池【创建对象的思考、字符串常量值(StringTable)、再谈String对象创建】、泛型进阶:通配符【通配符解决什么问题、通配符上界、通配符下界】)

接上次博客:初阶数据结构(10)(搜索树、搜索、Map 的使用、Set 的说明、哈希表、OJ练习【只出现一次的数字;复制带随机指针的链表;宝石与石头;坏键盘打字;前K个高频单词】)_di-Dora的博客-CSDN博客

目录

字符串常量池

创建对象的思考

字符串常量值(StringTable)

再谈String对象创建

1.直接使用字符串常量进行赋值

 2、通过new创建String类对象

3、intern方法

泛型进阶

什么是泛型

引出泛型

语法

泛型类的使用

通配符

通配符解决什么问题

通配符上界 

通配符下界


字符串常量池

创建对象的思考

在Java中,字符串常量池是一种特殊的运行时常量池,用于存储String类的字符串字面常量。

字符串常量池的主要目的是避免重复创建相同的字符串对象,从而节省内存提高运行效率

当在Java程序中使用字符串字面常量(如"hello")时,如果字符串常量池中已经存在相同内容的字符串对象,则不会创建新的对象,而是直接引用已存在的对象。这样做可以减少内存消耗,特别是在字符串频繁创建和使用的情况下。

举个例子,假设在代码中有两个字符串字面常量 "hello",那么它们都会被放入字符串常量池,并且只有一个实际的String对象用于表示这个内容。这样,当程序中其他部分使用了相同的字符串字面常量 "hello",都会共享这个已存在的String对象。

    public static void main6(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2); // true

        String s3 = new String("hello");

        System.out.println(s1 == s3); // false

        String s4 = new String("hello");

        System.out.println(s3 == s4); // false

        System.out.println(s1 == s3); // false

    }

存储字符串常量的时候,会先检查当前常量池是否存在你所要存储的常量。

 

在Java程序中,由于字面类型的常量经常频繁使用,为了使程序的运行速度更快、更节省内存,Java为8种基本数据类型和String类都提供了常量池

“池"是编程中的一种常见的,重要的提升效率的方式,我们会在未来的学习中遇到各种"内存池","线程池",数据库连接池"....

比如:家里给大家打生活费的方式:
1. 家里经济拮据,每月定时打生活费,有时可能会晚,最差情况下可能需要向家里张口要,速度慢
2. 家里有矿,一次性打一年的生活费放到银行卡中,自己随用随取,速度非常快

其中方式2,就是池化技术的一种示例,钱放在卡上,随用随取,效率非常高。

常见的池化技术比如:数据库连接池、线程池等。

为了节省存储空间以及程序的运行效率,Java中引入了:

为了节省存储空间以及提高程序的运行效率,Java中引入了以下三种常量池:

1. Class文件常量池:
每个Java源文件编译后会生成对应的Class文件。这个Class文件中会包含一个常量池,称为Class文件常量池。这个常量池是一种表格数据结构,用于保存当前类中的字面常量(如字符串、数字等)以及符号引用(如类和方法的全限定名、字段的名称等)。字面常量指的是在代码中直接出现的常量值,例如:"Hello, World!"。而符号引用是一种间接引用,它通过索引或指针指向在运行时常量池中的具体信息。Class文件常量池的主要目的是在编译时进行一些优化,避免重复存储相同的常量,减少内存占用和字节码的大小。

2. 运行时常量池:
在Java程序运行过程中,Class文件会被加载到内存中,并形成运行时数据结构,其中就包括运行时常量池。每个类在内存中都有一份对应的运行时常量池。运行时常量池包含了从Class文件常量池中复制过来的信息,同时还可能会动态生成一些附加的运行时常量,用于支持动态语言特性或者字节码指令的执行。运行时常量池的主要目的是提供在运行时快速访问常量的能力,以及支持动态性和反射等特性。

3. 字符串常量池:
字符串常量池是运行时常量池中的一部分。

我们前面其实已经提过了:在Java中,字符串是不可变的对象,为了节省内存并提高性能,字符串常量池被引入。当你创建一个字符串常量时(例如使用双引号括起来的字符串字面值),Java会首先检查字符串常量池中是否已经有相同值的字符串对象。如果有,则返回常量池中的对象引用,而不是创建一个新的字符串对象。这样可以避免创建重复的字符串对象,从而节省内存空间。

使用字符串常量池的优势在于:

1. 节省内存:避免了创建大量相同内容的字符串对象,降低了内存占用。

2. 提高效率:由于字符串常量池中的对象是可复用的,当需要创建相同内容的字符串时,可以直接引用常量池中的对象,避免了对象的重复创建和销毁过程,从而提高了程序的执行效率。

注意:虽然字符串常量池在一定程度上提高了程序的性能,但过度的使用字符串常量可能会导致一些问题。例如,如果在程序中大量使用字符串常量拼接,每次拼接都会生成一个新的String对象,这种情况下使用StringBuilder或StringBuffer等可变字符串类更合适,因为它们可以避免创建过多临时的String对象。

字符串常量值(StringTable)

字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable(一种高效用来进行查找的数据结构,后续会详细介绍)

不同JDK版本下字符串常量池的位置以及默认大小是不同的:

JDK版本字符串常量池位置  大小设置    
Java6   (方法区)永久代 固定大小:1009    
Java7  堆中可设置,没有大小限制,默认大小:60013  
Java8堆中可设置,有范围限制,最小是1009    

关于方法区、堆等内存结构的具体局部,后序JVM中会详细介绍。

再谈String对象创建

由于不同JDK版本对字符串常量池的处理方式不同,此处在Java8 HotSpot上分析

1.直接使用字符串常量进行赋值

 

 2、通过new创建String类对象

 结论:只要是new的对象,都是唯一的。

通过上面例子可以看出,使用常量串创建String类型对象的效率更高,而且更节省空间,用户也可以将创建的字符串对象通过intern方式添加进字符串常量池中。

3、intern方法

intern方法是一个native方法(底层使用C++实现,看不到其实现的源代码)。

intern() 方法是用来主动将一个字符串对象加入到常量池中的。当调用字符串的 intern() 方法时,JVM 会首先检查常量池中是否已经存在相同内容的字符串,如果存在则直接返回常量池中的引用,否则将该字符串添加到常量池中,并返回常量池中的引用。

public class InternExample {
    public static void main(String[] args) {
        String str1 = "Hello"; // 字符串"Hello"在编译时就会放入常量池
        String str2 = new String("Hello"); // 创建一个新的String对象,内容也是"Hello"

        System.out.println(str1 == str2); // false,因为两个对象在堆内存中是不同的

        String str3 = str2.intern(); // 将str2加入常量池,并返回常量池中的引用
        System.out.println(str1 == str3); // true,str3指向了常量池中的"Hello",与str1相同
    }
}
    public static void main6(String[] args) {
        char[] ch = new char[]{'a', 'b', 'c'};
        String s1 = new String(ch); // s1对象并不在常量池中
        String s2 = "abc"; // "abc" 在常量池中存在了,s2创建时直接用常量池中"abc"的引用
        System.out.println(s1 == s2);
    }


    // false

 

    public static void main6(String[] args) {
        char[] ch = new char[]{'a', 'b', 'c'};
        String s1 = new String(ch); // s1对象并不在常量池中
        //把这句话放开以后:--->变为true
        s1.intern(); // s1.intern();调用之后,会将s1对象的引用放入到常量池中
        String s2 = "abc"; // "abc" 在常量池中存在了,s2创建时直接用常量池中"abc"的引用
        System.out.println(s1 == s2);
    }
    
    // true

需要注意的是,虽然 intern() 方法可以减少重复字符串对象的内存占用,但滥用它也可能导致常量池过度膨胀,增加内存开销。在某些情况下,过度使用 intern() 方法可能会影响性能,因此在使用时需要谨慎考虑。

泛型进阶

我们之前已经简单地讲过泛型:数据结构初阶(1)(一些学习数据结构所需掌握的先导知识:数据结构的基本封装、包装类、装箱与拆箱、泛型【泛型的编译——擦除机制、泛型的上界、可类型推导的和不可类型推导的泛型方法、裸类型】、List简介)_di-Dora的博客-CSDN博客

什么是泛型

一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

                                                                             ----- 来源《Java编程思想》对泛型的介绍。

泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型

从代码上讲,就是对类型实现了参数化。

引出泛型

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?

虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

语法

    class 泛型类名称<类型形参列表> {
        // 这里可以使用类型参数
    }
    class ClassName<T1, T2, ..., Tn> {
    
    }


    class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
        // 这里可以使用类型参数
    }
    class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
        // 可以只使用部分类型参数
    }

泛型类的使用

泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

注意:泛型只能接受类,所有的基本数据类型必须使用包装类!

简单回顾过后,我们来看看关于泛型新的知识:

通配符

? 用于在泛型的使用,即为通配符

通配符解决什么问题

    class Message<T> {
        private T message;

        public T getMessage() {
            return message;
        }

        public void setMessage(T message) {
            this.message = message;
        }
    }


    public class Test2 {

        public static void fun(Message<String> temp){
            System.out.println(temp.getMessage());
        }
        public static void main1(String[] args) {
            Message<String> message = new Message<>();
            message.setMessage("你好!");
            fun(message);


            Message<Integer> message2 = new Message<>();
            message2.setMessage(18);
            fun(message2);
        }

    }

这个时候,fun(message)将出错,因为指定的 fun(Message<String> temp),而非fun(Message<Integer> temp)。

我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符"?"来处理:

    class Message<T> {
        private T message;

        public T getMessage() {
            return message;
        }

        public void setMessage(T message) {
            this.message = message;
        }
    }


    public class Test2 {
        // 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
        public static void fun(Message<?> temp){
            System.out.println(temp.getMessage());
        }
        public static void main1(String[] args) {
            Message<String> message = new Message<>();
            message.setMessage("你好!");
            fun(message);


            Message<Integer> message2 = new Message<>();
            message2.setMessage(199);
            fun(message2);
        }
    }

注意:使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改 

在"?"的基础上又产生了两个子通配符:

  • ? extends 类:设置通配符上限
  • ? super 类:设置通配符下限

通配符上界 

语法:

<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类

    class Food {

    }
    class Fruit extends Food {

    }
    class Apple extends Fruit {

    }
    class Banana extends Fruit {

    }


    class Message<T> {
        private T message;

        public T getMessage() {
            return message;
        }

        public void setMessage(T message) {
            this.message = message;
        }
    }

    public class Test2 {


        public static void funExtends(Message<? extends Fruit> temp){
            System.out.println(temp.getMessage());

            //通配符的上界来说 是不可以进行修改元素的
            /*
            temp.setMessage(new Banana()); //无法修改!
            temp.setMessage(new Apple()); //仍然无法修改!
            */

            //向上转型  这样是可以的
            Fruit fruit = temp.getMessage();
            
            //这样是不可以的,因为无法确定
            Banana banana = temp.getMessage();

        }
        public static void main(String[] args) {
            Message<Apple> message1 = new Message<>();
            message1.setMessage(new Apple());

            Message<Banana> message2 = new Message<>();
            message2.setMessage(new Banana());

            funExtends(message1);
            funExtends(message2);

        }
    }

此时无法在fun函数中对temp进行添加元素,因为temp接收的是Fruit和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素。

所以通配符的上界,不能进行写入数据,只能进行读取数据。

通配符下界

语法:

<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
 class Food {

    }
    class Fruit extends Food {

    }
    class Apple extends Fruit {

    }
    class Banana extends Fruit {

    }

    class Plate<T> {
        private T plate ;

        public T getPlate() {
            return plate;
        }

        public void setPlate(T plate) {
            this.plate = plate;
        }
    }

    

    public class Test2 {

    public static void funSuper(Plate<? super Fruit> temp){
            // 此时可以修改!!添加的是Fruit 或者Fruit的子类
            temp.setPlate(new Apple());//这个是Fruit的子类   向上转型
            temp.setPlate(new Banana());//这个是Fruit的本身

            //Fruit fruit = temp.getPlate(); //不能接收,这里无法确定是哪个父类
            System.out.println(temp.getPlate());//只能直接输出
        }

        public static void main(String[] args) {
            Plate<Fruit> message1 = new Plate<>();

            Plate<Food> message2 = new Plate<>();

            funSuper(message1);
            funSuper(message2);
        }
    }

所以通配符的下界,不能进行读取数据,只能写入数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值