2.4.3、引用数据类型
引用数据类型用于存储对象的引用(一个内存地址),而不是实际的值。引用数据类型大致包括:类、 接口、 数组、 枚举、 注解等
它和基本数据类型的最大区别就是:
-
基本数据类型是直接保存在栈中的
-
引用数据类型在栈中保存的是一个地址引用,这个地址指向的是其在堆内存中的实际位置。(栈中保存的是一个地址,而实际的内容是在堆中,通过地址去找它实际存放的位置)
引用数据类型在创建时,系统会分配两块空间,一块存放引用地址,一块存放实际的对象。
引用数据类型赋值给另一个变量时,赋的是内存地址,而不是实际的数据
如果两个引用数据的变量指向同一个对象时,一个变量修改值,另一个变量的值一样会受到影响。
每个方法执行时都会创建一个方法栈,方法内的变量都会放入栈内存中,方法执行结束,栈销毁,但是因为引用数据类型创建的成本通常比较大,所以才会将引用数据类型存放在堆中,因为堆中的对象不会随方法栈的销毁而回收,所以对象放在堆中,可以反复利用。只有当引用数据类型的数据没有被任何变量引用时,垃圾回收机制才会考虑将其回收。
1、String字符串
字符串是由多个字符组成的有序序列,属于Unicode字符序列。这些字符可以是字母、数字、标点符号或其他任何字符。
字符串并非是一个单独的数据类型,而是Java类库中的一个预定义的类,类名叫做String。在 Java 中使用字符串就是通过这个 String 类来创建的,它内部提供了很对操作字符串的相关方法,所以String是引用数据类型。
Java语言中字符串必须包含在一个双引号( " ") 中。
字符串类的构造方法:
1、public String():无参构造方法
2、public String(String original):通过给定字符串的副本构造新字符串
3、public String(char value[]):通过字符数组创建字符串
4、public String(char value[], int offset, int count) :截取指定字符数组长度创建
5、String(byte bytes[]) :使用操作系统的默认字符集,通过字节数组构建字符串
6、public String(byte bytes[], int offset, int length):截取指定字节数组长度创建
7、public String(byte bytes[], Charset charset):指定字符集,通过字节数组构建字符串
8、public String(StringBuffer buffer):通过StringBuffer构建字符串
9、public String(StringBuilder builder):通过StringBuilder构建字符串
两种不同的创建形式:
1、使用字面常量:String str1 = "name"; //这种方式创建的字符串对象会被存储在字符串常量池中。如果常量池中已经存在相同内容的字符串,则不会再创建新的对象,而是直接返回对应的引用。
2、使用new关键字:String str2 = new String("name"); //这种方式会在堆内存中创建一个新的字符串对象。即使字符串常量池中已经存在相同内容的字符串对象,也会再创建一个新的对象,只不过其值是从常量池中复制过来的。String 类是不可改变的,所以你一旦创建了 String 对象,那它的值就无法改变了,重新赋值只是改变存储的数据地址
区别:
- 直接赋值的方式可以节省内存空间,因为它会复用字符串常量池中已有的字符串对象,而不会每次都创建新的对象。
- 使用构造方法的方式可以保证每次都创建一个新的字符串对象,这样可以避免因为字符串常量池的共享而导致的一些安全问题
- 直接赋值的方式创建的字符串对象是不可变的,也就是说一旦创建了就不能修改其内容。如果需要对字符串进行频繁的修改,可以使用 `StringBuffer` 或 `StringBuilder` 类。
- 使用构造方法的方式创建的字符串对象也是不可变的,但是可以通过反射机制来修改其内容。但是这种做法是不推荐的,因为它会破坏字符串的不可变性和安全性。
- 直接使用字面常量赋值:最常见和推荐的方式。(字符串常量池(String Pool)是位于堆(Heap)中的一个特殊内存区域,用于存储字符串字面量。)
- 当通过字面量创建字符串时,JVM 会首先检查字符串常量池中是否已经存在相同的字符串。
- 如果存在,将引用池中的现有字符串,而不会创建新的字符串对象。
- 如果不存在,JVM 会在字符串常量池中创建一个新的字符串对象,并将引用返回。
- 使用new关键字,通过构造方法创建:
- JVM在常量池中查找是否已经存在相同的字符串。(如果常量池中不存在则创建)。
- 然后在堆中创建一个包含与常量池中字符串相同内容的新对象。
- 最后引用这个新创建的字符串对象。
注意:String不是基本数据类型,它是一个类
基本数据类型只有:byte、short、int、long、float、double、Boolean、char8种
Java.lang.String类是final类型的,所以String不能继承和修改。
三种操作字符串的类:String、StringBuffer、StringBuilder
在 Java 中,处理字符串的三个主要类是 String、StringBuffer 和 StringBuilder。这些类提供了不同的方式来创建和操作字符串,每个类都有其特定的用途和性能特点。
String
- 不可变性:String 类是由 final 修饰的,其值不可变。
- 效率和安全性:在多线程环境中是安全的,但是,频繁的修改操作(如拼接、替换)可能会导致性能问题,因为每次修改都会生成新的 String对象。
- 字符串池:由于 String 对象是不可变的,Java 虚拟机维护了一个特殊的字符串池,以节省内存和提高性能。
StringBuffer
- 可变性:StringBuffer 是可变的,可以在不创建新对象的情况下修改字符串的内容。
- 效率和安全性:StringBuffer 中的大多数方法都是同步的,这使得它在多线程环境中是线程安全的。但这也意味着它在单线程环境中可能比 StringBuilder 慢。(StringBuilder>StringBuffer>String)
- 适用场景:当需要在多线程环境中频繁修改字符串时,StringBuffer 是一个合适的选择。
StringBuilder
- 可变性:StringBuilder与 StringBuffer类似,也是可变的,允许在不创建新对象的情况下修改字符串。
- 效率和安全性:StringBuilder不是线程安全的。它没有同步方法,因此在单线程环境中性能更好。
- 适用场景:在单线程环境中,或者不需要考虑线程安全的情况下,StringBuilder是更高效的选择。
常用的方法
-
字符串拼接
1、用 + 来拼接多个字符串(最直接和常见的字符串拼接方式,特别是对于简单的字符串拼接操作)(也可以拼接其他的数据类型,但要注意拼接过程中可能会涉及到数据的运算) 2、str1.concat(str2):用于拼接两个字符串,返回的是一个新字符串,原本的两个值不会变。 3、通过 format 格式化字符串 (适合需要插入多个变量或进行复杂格式化的场景。) String result = String.format("%s %s", "Hello,", "World!"); // 结果: "Hello, World!" 4、通过 join 方法拼接字符串 (适用于将数组或集合中的多个字符串元素拼接成一个单一的字符串,可指定拼接符) String[] words = {"Hello", "world"}; String greeting = String.join(", ", words) + "!";
-
2、获取字符串长度: str.length(); 3、获取字符串中的单个字符: str.charAt(i); //i是字符串内每个字符的索引位置从0开始,length-1结束 4、比较字符串 不忽略字母大小写:str1.equals(str2); 忽略字母大小写:str.equalsIgnoreCase(str2); 5、获取某一字符的位置: 查找指定字符第一次出现的位置:str.indexOf("指定的字符") 从指定位置(int)开始查找指定字符第一次出现的位置:str.indexOf("指定的字符",int) 查找指定字符最后一次出现的位置:str.lastIndexOf("指定的字符") 从指定位置(int)开始往前查找指定字符第一次出现的位置:str.lastIndexOf("指定的字符",int); 6、字符串的截取: 从指定位置截取到最后:str.subString(int) 区间[0,int) 从指定位置截取到指定位置:str.subString(int1,int2); 区间[int1,int2) 7、去除空格: 去除首位空格:str.trim() 去除所有空格(将所有空格替换):str.replaceAll("//s",""); 8、替换字符串中指定的字符 将所有符合指定的字符串都替换成新字符串:str.replaceAll("原字符串","新字符串") 可使用字符串格式 将指定字符替换换成其他字符:str.replace('原字符','新字符') 替换单个字符 将符合指定字符串的第一个替换成新字符串,后续的不变:str.replaceFrist("原字符串","新字符串"); 9、判断字符串的开头与结尾(返回的是布尔值): 判断某字符串是否以**开头:str.startsWith("**") 判断字符串从指定位置开始是否以**开头:str.startsWith("**",int) 判断某字符串是否以**结尾:str.endsWith("**"); 10、字符串中大小写转换: 大写变小写:str.toLowerCase() 小写变大写:str.toUpperCase(); 11、分割字符串: 以指定字符分割:str.split('分隔符') 以指定字符分割指定次数:str.split('分隔符',int); 12、判断是否为空字符串: str.isEmpty(); 13、字符串的格式化: String str = String.formart("",); //和格式化打印类似 14、空串和null 长度为0的字符串:String s = "";(表示内容为空) 值为null的字符串:String s = null;(表示目前没有任何对象与该变量关联) 15、contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true。 16、getBytes(Charset charset):按指定编码将字符串转化为存byte数组。
2、String底层
String类的底层设计是 Java 核心库的关键部分。String的特性就是基于它底层设计提现出来的:
-
不可变性:String类被声明为
final
,包含了final的先关特性。 -
内部的数据在Java 9 之前的版本保存字符是使用 char[] value,从 Java 9 开始,为了优化内存使用和提高性能,String类的内部已经从
char[]
更改为byte[]
,并引入了一个额外的字段来表示编码格式。这做的好处是可以减少内存使用量,且在字符集上更加灵活。- LATIN1:如果字符串只包含Latin-1字符(每个字符可以用一个字节表示),coder字段为 LATIN1,字符串数据将以单字节形式存储在 byte[]数组中。
- UTF16:如果字符串包含非Latin-1字符(需要两个字节表示),coder字段为 UTF16,字符串数据将以双字节形式存储在 byte[]数组中。
-
字符串池:
- 当使用字符串字面量创建字符串时,它们被存储在特殊的内存区域——字符串池中。这有助于节省内存,相同的字符串字面量只存储一次。
String提供了一个
intern()
方法,该方法返回字符串对象的规范化表示。如果字符串池中已经包含了与此String
对象相等的字符串,则返回池中的字符串。否则,此String
对象将被添加到池中,并返回对此String
对象的引用。 -
哈希缓存:String有一个私有的
hash
字段,用于缓存字符串的哈希码。由于字符串是不可变的,其哈希码也是不变的,这意味着只需要计算一次哈希码,然后可以多次使用,提高了性能。
-
字符串连接优化:在编译时,单纯的字面量连接(如 “Hello” + “World”)会被优化为一个单一的字符串字面量,不会产生新的对象,从而提高性能。
但是如果有变量参与拼接时,就会创建新的字符串对象。
public static void main(String[] args) { String a1 = "abc"; String a2 = "def"; String a3 = "abcdef"; String a4 = "abc"+"def"; //会直接优化成"abcdef",该字面量常量池中存在,会直接引用 String a5 = "abc"+a2; //存在对象时,会创建新对象,而不是直接引用 String a6 = a1+a2; String a7 = a1+"def"; System.out.println(a3 == a4); //true System.out.println(a3 == a5); //false System.out.println(a3 == a6); //false System.out.println(a3 == a7); //false }
-
在早期的Java版本中,字符串的拼接主要是通过StringBuffer实现的,通过append方法添加内容,最后通过toString方法转换成String对象。这种方式在拼接多个字符串时,效率较低。
-
从JDK 5开始,Java引入了StringBuilder替代StringBuffer进行字符串拼接(或在多线程环境中使用 StringBuffer)来完成的。
-
Java 9 引入了新的方式来处理编译期的字符串拼接,使用
invokedynamic
指令,主要作用是延迟方法解析,直到运行时再确定具体的调用目标。
- 编译阶段:当编译器遇到字符串拼接表达式时,会执行invokedynamic指令,该指令被绑定到一个引导方法makeConcatWithConstants,这里是StringConcatFactory的一个静态方法。
- 运行阶段:
- 在程序运行时,当执行到invokedynamic指令时,会调用一个配置好的引导方法。对于字符串拼接,这个引导方法是
makeConcatWithConstants
方法。 - 该方法会基于传入的参数和可能的优化策略(StringBuilder、StringBuffer、平铺式拼接、或者其他策略)动态生成并返回一个CallSite对象(包含了实际的方法句柄)。
- CallSite对象有一个MethodHandle,该方法指向最终用于执行字符串拼接的方法。
- 一旦CallSite和MethodHandle被创建,就会本缓存起来,用于后续的所有相同类型的字符串拼接操作,从而提高性能。
- 在程序运行时,当执行到invokedynamic指令时,会调用一个配置好的引导方法。对于字符串拼接,这个引导方法是
-
-
其他类(如
StringBuilder
,StringBuffer
,Charset
等)紧密集成,提供了丰富的功能和灵活性。
3、块字符串
块字符串是Java 13引入的一种新特性,也被称为多行字符串文本块。传统的多行字符串需要使用引号和转义字符,而块字符串消除了这些复杂性。
public static void main(String[] args) {
String s1 ="hello "
+"java\n"
+",对字符串进行拼接";
System.out.println(s1);
}
.
在此前多行字符串拼接使用+和转移字符,但如果内容复杂的话(比如拼接JSON字符串、HTML页面等等),会导致代码繁琐降低可读性。
所以增加了字符串的块式写法。格式是 三个英文引号:
public static void main(String[] args) {
String s2 = """
hello java
,不用拼接,且打印时会自动换行
""";
System.out.println(s2);
}
在使用块字符串时需要注意下面的问题:
- 换行符处理:每一行都保留换行符。
- 缩进管理:自动检测和删除每一行的共同前导空白,这有助于保持代码结构整洁。
- 拼接:块字符串可以与其他字符串拼接,包括传统的字符串和其他块字符串。
- 格式化:如果需要在块字符串中插入变量或表达式的值,需要使用
String.format
方法或其他模板库。- 尾随空白:尾随空白(每行末尾的空格)默认是被忽略的。
- 阅读性:虽然块字符串使得编写多行文本变得容易,但应该注意保持代码的可读性。对于非常长的块字符串,考虑将它们存储在外部文件中可能是更好的选择。
- IDE 支持:老版本的 IDE 可能不支持这一特性。
4、数组
数组是编程中常用的一种数据结构,它不同于我们之前用的数据类型,数组可以同时存储多个相同类型的数据。
数组是一个固定长度、存储相同数据类型
的一种数据结构,在内存中的体现是一个连续的内存空间
。有索引值,从0开始,可通过索引快速快速查询、修改相应元素
。
数组在创建时需要指定数组存放元素的数据类型(基本数据类型、引用数据类型),需要指定数组的长度。
注意:在创建数组时Java允许创建长度为0的数组
int[] its1 = new int[0]; int[] its1 = {};
长度为0的数组和值为null不一样:
- 长度为0的数组是一个真实存在的对象,它在内存中有一个具体的位置,但它不包含任何元素。这种数组被称为“空数组”。创建一个长度为0的数组后,你可以对其进行操作,比如遍历(虽然实际上不会执行任何迭代),获取其长度(结果为0),也可以将其作为参数传递给方法。长度为0的数组通常用于表示一个“空”的集合,而不是一个“不存在”的集合。
- 值为null的数组表示该数组没有引用任何对象,它在内存中没有指向任何位置。null是一个特殊的值,用于表示变量没有指向任何有效的内存地址。尝试访问一个值为null的数组的任何属性或方法(如尝试获取其长度或遍历它)都会抛出空指针异常NullPointerException。
对数组来说,它本身是引用数据类型,数组内保存的元素可以是任意类型的,但是一个数组只能保存一种类型。它引用的是一个连续的空间,引用的就是这块连续空间的首地址,而空间内可以保存各种类型数据。
基本特点:
1、数组是有界限的[0,length-1],其长度定义后不可更改,所以在使用过程中经常会遇到数组下标越界的异常 (java.lang.ArrayIndexOutOfBoundsException),这种设计简化了数组的内部管理,但也意味着灵活性有限。
2、数组的数据类型需要在创建时定义,一旦定义后,只能保存着一种类型的数据(可以定义任意类型的数据),确保了类型安全。
3、数组属于引用类型,存放在堆中,栈中的数据实际上保存的是对象在堆中的地址,Java会为该数组在堆上分配一块连续的内存空 间。这块空间的大小取决于数组的长度和元素的数据类型。
int[] its1 = new int[5];
int[] its2 = its1l;
its1和its2指向的地址是同一个。
4、直接索引访问:提供了通过索引直接访问其元素的能力。这是高效数据检索和操作的基本特性,特别是在需要快速访问的场景中。(Java数组使用从零开始的索引。)
5、数组在初始化后,即便没有给值,也会有初始化的默认值,数据类型不同其默认值不同
除此之外,Java还有二维数组、多维数组。二维数组是一种数组的数组,通常用于表示表格形式的数据,如矩阵。每个元素本身也是一个数组,这使得二维数组可以通过两个索引来访问数据:一个用于行,另一个用于列。
Arrays类:
位于 java.util 包中,主要包含了操纵数组的各种方法
常用的函数:
1、打印数组元素 toString、deepToString Arrays.toString(数组名); //一维数组 Arrays.deepToString(数组名); //多维数组 2、数组元素升序排列:sort Arrays.sort(数组名); //数值按大小排序,String类型数组按照字典顺序,数字、大写字母、小写字母汉字的顺序 3、填充值:fill Arrays.fill(数组名,值); //用指定的值填充数组全部的元素 Arrays.fill(数组名,start,end,值); //数组下标为[start,end)的元素全部填充为指定值 4、比较数组:equals Arrays.equals(数组1,数组2); 5、复制数组 copyOf、copyOfRange copyOf(数组名,复制的长度); //超过要复制的数组长度时,整型用0,char用null,小于数组长度截取数组 copyOfRange(数组,start,end); //[start,end)截取数组进行复制 6、对排序后的数组进行二分法查询 binarySearch binarySearch(数组,值) //如果要搜索的值在数组内,则返回该值的索引,否则返回-1 binarySearch(数组,start,end,值) //在[start,end)范围内搜索相应的值
**