接上次博客:初阶数据结构(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对象。
字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable(一种高效用来进行查找的数据结构,后续会详细介绍)
不同JDK版本下字符串常量池的位置以及默认大小是不同的:
JDK版本 | 字符串常量池位置 | 大小设置 |
Java6 | (方法区)永久代 | 固定大小:1009 |
Java7 | 堆中 | 可设置,没有大小限制,默认大小:60013 |
Java8 | 堆中 | 可设置,有范围限制,最小是1009 |
关于方法区、堆等内存结构的具体局部,后序JVM中会详细介绍。
由于不同JDK版本对字符串常量池的处理方式不同,此处在Java8 HotSpot上分析
结论:只要是new的对象,都是唯一的。
通过上面例子可以看出,使用常量串创建String类型对象的效率更高,而且更节省空间,用户也可以将创建的字符串对象通过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 {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName extends ParentClass {
// 可以只使用部分类型参数
}
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!
简单回顾过后,我们来看看关于泛型新的知识:
? 用于在泛型的使用,即为通配符
class Message {
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 message = new Message<>();
message.setMessage("你好!");
fun(message);
Message message2 = new Message<>();
message2.setMessage(18);
fun(message2);
}
}
这个时候,fun(message)将出错,因为指定的 fun(Message
我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符"?"来处理:
class Message {
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 message = new Message<>();
message.setMessage("你好!");
fun(message);
Message message2 = new Message<>();
message2.setMessage(199);
fun(message2);
}
}
注意:使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
在"?"的基础上又产生了两个子通配符:
语法:
extends 上界>
extends Number>//可以传入的实参类型是Number或者Number的子类
class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Message {
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 message1 = new Message<>();
message1.setMessage(new Apple());
Message 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 {
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 message1 = new Plate<>();
Plate message2 = new Plate<>();
funSuper(message1);
funSuper(message2);
}
}
所以通配符的下界,不能进行读取数据,只能写入数据。