题目:
1.String直接赋值和new的对象个数,内存
2.为什么字符串类型数据输出的时候,输出结果是字符串的值而不是字符串对象的地址(NEW出来的字符串对象)
3.+连接String的对象个数
4.查找api学习使用split方法
5.StringBuffer和String的相同不同
6.8种基本数据类型的数据 存放在哪个区
7.常量池的理解,8个包装类和String和其他类生成对象的不同之处
8.String,基本类型,包装类的转换√
9.不变模式,如果String不是不变模式会出现什么问题
10.equals和= = 的对比,String类和其他类 举例
11.源码提升:
打印地址的四种方法及源码
找到Object类equals、toString、hashcode方法的内容
找到String类equals、toString方法内容与Object类中的不同,强不变模式的实现,常用的集中构造方法
12.浅拷贝和深拷贝
1.String直接赋值和new的对象个数,内存
这里必须要讲一讲字符串常量池,他在Java7之后,在堆内存中了。这是为了更好得提高内存管理的效率,因为堆内存更容易进行垃圾回收和优化。
这个字符串常量池判重,如果有重复他就不会在开辟空间再来存储一遍一样的了。
但是这个new的话,只要你new一下,就会开辟一个空间,这样的话,尽管你的存储的字符串是是相同的,但是他们的地址不同还是会再存储一遍。
package code711;
public class code05 {
public static void main(String[] args) {
// 字符串字面量
String str1 = "hello"; //开辟一个内存
String str2 = "hello";//没有开辟
// new 关键字创建字符串对象
String str3 = new String("hello");//开辟一个
String str4 = new String("hello");//开辟一个
// intern() 方法
String str5 = str3.intern();
String str6 = str4.intern();
/**
* intern() 方法会检查字符串常量池中是否存在内容相同的字符串:
如果存在,返回池中的字符串对象。
如果不存在,将该字符串对象添加到池中,并返回引用。*/
// 比较引用
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str3 == str4); // false
System.out.println(str1 == str5); // true
System.out.println(str5 == str6); // true
}
}
package com.haina.lesson11;
public class StringTest03 {
public static void main(String[] args) {
//下面6句话创建了几个对象
/**
* 问下面语句创建了几个对象
* 答:字符串常量位于字符串常量池中,而字符串常量池在1.6 之前位于方法区中,在1.8之后
* 为于堆区中,而字符串常量池在设计的时候,为了减少内存,当字符串常量池中需要使用的字符串常量
* 已经存在的时候会直接调用这个常量,而常量池中没有这个字符串常量才会创建一个新的字符串,但是不管常量池中如何
* 只要有new 就会在堆区中产生一个对象所以产生多少个对象,要考虑堆区和常量池中
*/
String s1="hello";//1
String s2=new String();//1
String s3=new String("hel1o");//2
String s4=new String("hello world");//2
String s5="hello World";//1
String s6="hello world";//0
String s7="q"+"w"+"e"+"r"+"d"+"f";//编译器优化完为 String s7="qwerdf"
String s8="a"+"b"+"c"+"d"+"e"+"f";//编译器优化完为 String s7="abcdef"
}
}
2.为什么字符串类型数据输出的时候,输出结果是字符串的值而不是字符串对象的地址(NEW出来的字符串对象)
为什么String打印输出的是内容,而普通引用类型的对象输出的是地址呢 因为String从写了object的tostring方法,所以在调用输出的时候,会将字符串转换成数组字符。再将 字符数组输出到控制台上,所以输出的是字符串的内容,而普通引用变量调用的是object的tostring方法 打印输出的是完整的类路径+类名+@+十六进制地址值
![](https://i-blog.csdnimg.cn/direct/8a706402378a4b02bfedbe7776452394.png)
3.+连接String的对象个数
1. 编译期常量字符串拼接
如果拼接的字符串是编译期常量(即在编译时就可以确定其值),编译器会在编译阶段对这些字符串进行优化,直接将其合并为一个字符串。
String str1 = "Hello, " + "world!";
//上述代码在编译后等价于:
String str1 = "Hello, world!";
//在这种情况下,只会创建一个字符串对象。
2. 运行期字符串拼接
如果拼接的字符串包含变量,或者在运行时才能确定其值,Java 会使用 StringBuilder 或 StringBuffer(在多线程环境中)来进行拼接操作。
String str2 = "Hello, ";
String str3 = "world!";
String str4 = str2 + str3;
在这种情况下,Java 会执行以下步骤:
创建一个 StringBuilder 对象。
将 str2 的值追加到 StringBuilder 中。
将 str3 的值追加到 StringBuilder 中。
调用 toString() 方法将 StringBuilder 转换为一个新的 String 对象。
这意味着在运行时会涉及到以下对象:
一个 StringBuilder 对象。
一个新的 String 对象(用于存储拼接后的结果)。
3. 多个字符串拼接
当有多个字符串拼接时,情况类似,依然使用 StringBuilder 来处理。
String str5 = "Hello, ";
String str6 = "world";
String str7 = "!";
String result = str5 + str6 + str7;
在这种情况下,Java 会创建一个 StringBuilder 对象,并逐个将 str5、str6 和 str7 的值追加到 StringBuilder 中,最终转换为一个新的 String 对象。4. 使用 StringBuilder 显式拼接
为了避免在运行时频繁创建 StringBuilder 对象,可以显式地使用 StringBuilder 进行拼接操作,这在需要进行大量字符串拼接时特别有用。
String str8 = "Hello, ";
String str9 = "world";
String str10 = "!";
StringBuilder sb = new StringBuilder();
sb.append(str8);
sb.append(str9);
sb.append(str10);
String result2 = sb.toString();
在这种情况下,只会创建一个 StringBuilder 对象和一个最终的 String 对象,性能更优。
public class StringConcatExample {
public static void main(String[] args) {
// 编译期常量拼接
String str1 = "Hello, " + "world!";
// 运行期字符串拼接
String str2 = "Hello, ";
String str3 = "world!";
String str4 = str2 + str3;
// 多个字符串拼接
String str5 = "Hello, ";
String str6 = "world";
String str7 = "!";
String result = str5 + str6 + str7;
// 使用 StringBuilder 显式拼接
StringBuilder sb = new StringBuilder();
sb.append(str5);
sb.append(str6);
sb.append(str7);
String result2 = sb.toString();
// 输出结果
System.out.println(str1); // Hello, world!
System.out.println(str4); // Hello, world!
System.out.println(result); // Hello, world!
System.out.println(result2); // Hello, world!
}
}
总结
编译期常量拼接:在编译阶段优化,只创建一个字符串对象。
运行期字符串拼接:使用 StringBuilder,涉及一个 StringBuilder 对象和一个新的 String 对象。
显式使用 StringBuilder:可以优化性能,尤其是在大量字符串拼接时。
String Buffer是一个线程安全的,加了同步,StringBuilder 线程不安全。
4.查找api学习使用split方法
先附上源码:
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
实现细节
1.快速路径优化:首先检查正则表达式是否是单字符字符串且该字符不是正则表达式的元字符,或者是两字符字符串且第一个字符是反斜杠,第二个字符不是 ASCII 数字或字母。
如果满足这些条件,则可以使用优化的路径,而不需要使用正则表达式进行拆分。
2.查找分隔符:使用 indexOf 方法查找分隔符在字符串中的位置。
如果找到分隔符,则将当前位置之前的字符串添加到结果列表中,并更新偏移量 off。
如果找不到分隔符且偏移量为零,则返回包含原始字符串的数组。
3.处理限制:如果设置了 limit,则需要确保结果数组的长度不超过 limit。
如果 limit 为零,则去除结果数组末尾的空字符串。
4.结果构造:将结果列表转换为数组并返回。
5.使用正则表达式:如果不满足快速路径优化的条件,则使用 Pattern.compile(regex).split(this, limit) 进行字符串拆分。
正则表达式是一个比较重要的东西,比较复杂,有时间会写一个博客。
5.StringBuffer和StringBuilder的相同不同
相同点
1.基本功能:
两者都用于创建和操作可变的字符串。它们提供的方法如 append(), insert(), delete(), reverse(), replace(), 和 substring() 等,功能几乎完全一致。它们的初始容量和增长策略相同,都可以指定初始容量,且在容量不足时会自动扩容。
2.继承关系:
它们都继承自 AbstractStringBuilder 类,并实现了 CharSequence 接口。
不同点
1.线程安全性:
StringBuffer 是线程安全的。它的方法是同步的(使用 synchronized 关键字修饰),因此在多线程环境中可以安全使用。
StringBuilder 不是线程安全的。它的方法没有进行同步,因此在单线程环境中使用时更高效。
2.性能:
由于 StringBuffer 是线程安全的,它的同步机制会带来额外的性能开销。
StringBuilder 没有同步机制,因此在单线程环境中性能比 StringBuffer 更高。
package code711;
public class code07 {
public static void main(String[] args) {
// 使用 StringBuffer
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("Hello");
stringBuffer.append(" ");
stringBuffer.append("World");
System.out.println("StringBuffer: " + stringBuffer.toString());
// 使用 StringBuilder
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Hello");
stringBuilder.append(" ");
stringBuilder.append("World");
System.out.println("StringBuilder: " + stringBuilder.toString());
}
}
性能测试:
package code711;
public class code08 {
public static void main(String[] args) {
int iterations = 1000000;
// 测试 StringBuffer
long startTime = System.currentTimeMillis();
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < iterations; i++) {
stringBuffer.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuffer time: " + (endTime - startTime) + "ms");
// 测试 StringBuilder
startTime = System.currentTimeMillis();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < iterations; i++) {
stringBuilder.append(i);
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder time: " + (endTime - startTime) + "ms");
}
}
运行结果:
6.8种基本数据类型的数据 存放在哪个区
基本数据类型的局部变量:存储在栈上。
基本数据类型的成员变量:存储在堆上。
基本数据类型的静态变量:存储在方法区(或元空间)。
7.常量池的理解 8个包装类和String和其他类生成对象的不同之处
常量池(Constant Pool)是 Java 虚拟机(JVM)中的一部分内存,用于存放编译期生成的各种字面量(literal)和符号引用(symbolic references)。常量池在类加载后被创建,在类结构信息存储区域中,用于存储常量,包括:
字面量(Literal Constants):如文本字符串、被声明为 final 的常量值等。
符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
特点和作用
1.共享和节约空间:常量池中的内容是共享的,即多个实例可以引用相同的常量,这样节约了内存空间。
2.动态生成:在编译期间,Java 编译器会将所有的字面量和符号引用收集到常量池中。
3.存储位置:常量池存储在方法区(在 Java 8 及之前的版本称为永久代,Java 8 后称为元空间)中。
8.String,基本类型,包装类的转换
package code711;
public class code09 {
public static void main(String[] args) {
//String类型转换成基本类型
String str = "123";
int intValue = Integer.parseInt(str); // String 转换为 int
double doubleValue = Double.parseDouble(str); // String 转换为 double
/**String 转换为包装类
*通过包装类的构造方法或静态方法(如 valueOf())
* 可以将 String 类型的数据转换为对应的包装类。*/
String str1 = "123";
Integer integerObj = new Integer(str1); // String 转换为 Integer,过时的方法,不推荐使用
Integer integerObj2 = Integer.valueOf(str1); // 推荐使用 valueOf() 方法
/**基本类型转换为 String
可以使用 String 类的静态方法 valueOf() 或直接进行字符串拼接(String concatenation)将基本类型转换为 String。
*/
int a = 123;
String str2 = String.valueOf(a); // int 转换为 String
String str3 = Integer.toString(a); // 也可以使用 Integer 的 toString() 方法
/**包装类转换为 String
包装类可以直接调用它们的 toString() 方法来转换为 String。*/
Integer b = new Integer(123);
String str4 = integerObj.toString(); // Integer 转换为 String
}
}
9.不变模式,如果String不是不变模式会出现什么问题
不变模式: 不变模式(Immutable Pattern)是一种设计模式,用于创建不可变的对象,即一旦对象被创建后,其状态(属性)就不能被修改。在不变模式中,对象的所有域都是 final 类型,并且对象在构造后不能被修改。如果需要修改对象的状态,只能通过创建一个新的对象来实现,而不是直接在原对象上进行修改。
1.安全性问题:
不变性保证了 String 对象在创建后不能被修改。如果 String 是可变的,可能会导致在多线程环境下发生竞态条件(Race Condition),从而引发安全漏洞。
2.线程安全性问题:
因为 String 经常用于作为锁对象或者作为键值对中的键(如在哈希表中),如果 String 可变,可能会导致哈希表中的键在改变后无法被正确检索,从而破坏了线程安全性。
3.不可预期的行为:
如果 String 可变,代码逻辑依赖于 String 不变性的假设可能会失效,导致程序行为不可预测。比如,期望在传递参数或者在集合中存储字符串时,保持不变性可以确保对象状态不会在不知情的情况下被修改。
4.缓存问题:
String 类的不变性允许运行时系统对字符串进行缓存,以提高性能和节省内存。如果字符串是可变的,缓存会变得复杂并可能会导致内存泄漏或错误的缓存命中。
4.设计和优化问题:
如果 String 可变,代码可能需要更多的防御性编程,例如深度复制或额外的同步措施,以确保对象在传递和使用时不会被意外修改。
10.equals和= = 的对比,String类和其他类 举例
equals方法的实现原理 * String的equals比较的是两个字符串的内容是否相同,所以会将字符串转换为字符数组进行按位逐一 * 比较但是为了提高比较效率,equals 会进行三重判断,第一重比较两个对象的地址是否相同 * 如果相同则认为同一个对象,会直接返回true 否则进行第二重判断,第二重判断比较对象是不是String类型的对象 * 如果不是则直接返回false;如果是则进行第三重判断,第三重判断比较两个字符串长度是否相同,不同返回false * 如果相同则转换成字符数组进行注意比较 * * 注意不是字符串类型的对象使用equals 那么,equals比较就等于==
11.源码提升:
打印地址的四种方法及源码
找到Object类equals、toString、hashcode方法的内容
找到String类equals、toString方法内容与Object类中的不同,强不变模式的实现,常用的集中构造方法
package code711;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class code10 {
public static void main(String[] args) throws Exception {
//第一种toString()方法。
code10 code = new code10();
System.out.println(code.toString());
//第二种System.identityHashCode()
System.out.println(System.identityHashCode(code));
//第三种unsafe方法,不安全,不建议
Unsafe unsafe = getUnsafe();
long address = unsafe.getLong(code, 4L);
System.out.println(Long.toHexString(address));
//第四种使用 Instrumentation 接口
}
private static Unsafe getUnsafe() throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
}
object类里的toString();
object 类里面的equals()
object类里的hascode()
以下是String类里的:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
/**
12.浅拷贝和深拷贝
浅拷贝
浅拷贝创建一个新对象,这个新对象是原对象的精确副本,但它仅复制对象中的引用,而不是引用的对象本身。这意味着如果原对象内部的某个引用类型成员被修改,那么浅拷贝出来的新对象的相应成员也会受到影响。实现浅拷贝的方法
实现 Cloneable 接口并重写 clone() 方法。
使用构造函数复制。
以下是浅拷贝的示例:
import java.util.Arrays;
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class Person implements Cloneable {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) {
try {
Address address = new Address("New York");
Person person1 = new Person("John", address);
// 浅拷贝
Person person2 = (Person) person1.clone();
System.out.println(person1.name + " lives in " + person1.address.city);
System.out.println(person2.name + " lives in " + person2.address.city);
person2.address.city = "Los Angeles";
System.out.println(person1.name + " lives in " + person1.address.city);
System.out.println(person2.name + " lives in " + person2.address.city);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
/**输出:
复制代码
John lives in New York
John lives in New York
John lives in Los Angeles
John lives in Los Angeles
*/
从输出结果可以看到,修改了 person2 的 address.city 后,person1 的 address.city 也被修改了。这是因为 Person 的浅拷贝只复制了对象的引用,而不是引用的对象本身。
深拷贝
深拷贝不仅复制对象本身,还复制它所引用的所有对象。这样,即使修改了新对象中的引用类型成员,原对象中的相应成员也不会受到影响。实现深拷贝的方法
递归实现 clone() 方法。
使用序列化和反序列化。
以下是深拷贝的示例:
import java.io.*;
class Address implements Serializable {
String city;
Address(String city) {
this.city = city;
}
}
class Person implements Serializable {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
public Person deepClone() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Person) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
public class Main {
public static void main(String[] args) {
Address address = new Address("New York");
Person person1 = new Person("John", address);
// 深拷贝
Person person2 = person1.deepClone();
System.out.println(person1.name + " lives in " + person1.address.city);
System.out.println(person2.name + " lives in " + person2.address.city);
person2.address.city = "Los Angeles";
System.out.println(person1.name + " lives in " + person1.address.city);
System.out.println(person2.name + " lives in " + person2.address.city);
}
}
/**
输出:
John lives in New York
John lives in New York
John lives in New York
John lives in Los Angeles
*/
从输出结果可以看到,修改了 person2 的 address.city 后,person1 的 address.city 没有被修改。这是因为 Person 的深拷贝不仅复制了对象本身,还复制了它所引用的所有对象。
总结
浅拷贝:复制对象,但不复制内部对象的引用。修改副本的引用类型成员会影响原对象。
深拷贝:复制对象及其所引用的所有对象。修改副本的引用类型成员不会影响原对象。