目录
String解析
final的作用
String是否有长度限制
StringBuffer解析
StringBuilder解析
关键字、操作类相关
引用数据类型非常多大致包括:类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型。String类型就是引用类型。
String解析
JVM运行时会分配一块空间给String,字符串的分配和其他对象分配一样,需要消耗高昂的时间和空间,JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化,使用字符串常量池,创建字符串常量时,JVM先检查字符串常量池中有没有,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。String类是由final关键字修饰的,字符串具有不可变性,常量池中不会存在两个相同的字符串。
public class App {
public static void main(String[] args) {
String a = "111";
a = "222";
System.out.println(a);
}
}
引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。所以上面String a = “111”,表达的是变量a里保存了“111”这个对象的引用地址,变量是可以变的,不能变的是“111”。a="222",先去JVM常量池中查找,如果常量池中存在,就直接把对象的引用地址赋给a,如果不存在就重新创建一个对象,然后把对象的引用地址赋给a。
final的作用
当用final修饰一个类时,表明这个类不能被继承。final修饰的类中的成员变量可以根据需要设为final(类中所有成员方法都会被隐式地指定为final方法)。final修饰的方法表示此方法已经是“最后的、最终的”含义,即此方法不能被重写,但可以重载。重写的前提是子类可以从父类中继承此方法,如果父类中final修饰方法同时访问控制权限为private,会导致子类中不能直接继承到此方法,此时可以在子类中定义相同的方法名和参数(类的private方法会隐式地被指定为final方法)。当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化。如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。另外final修饰一个成员变量必须要初始化。有两种初始化方式(在声明的时候给其赋值,在其类的所有构造方法中为其赋值)
public class FinalDemo {
private final String name;//1
public FinalDemo(String name) {//2
this.name = name;//3
}
public FinalDemo() {//4
}
}
1处不会通过编译,要先给name初始化值才可以通过编译。
String几个常用方法源码
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
//啥都没有,就直接把当前字符串给你
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
//看到了吗?返回的居然是新的String对象
return new String(buf, true);
}
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
public String replace(char oldChar, char newChar) {
//如果两个是一样的,那就必要替换了,所以返回this
if (oldChar != newChar) {
int len = value.length;
int i = -1;
//把当前的char数组复制给val,然后下面基于val来操作
char[] val = value;
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
//创建一个新的char数组
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//创建一个新的String对象
return new String(buf, true);
}
}
return this;
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//正常返回的都是新new出来的String对象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果是该字符串中包含了空格,调用substring方法,否则就是啥都没干原本返回
//就是如果字符串里有空格,那么还是新生一个String对象返回
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
无论是concat、replace、substring还是trim方法的操作都不是在原有的字符串上进行的而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,任何变化性的操作都会生成新的对象。
public class App {
public static void main(String[] args) {
String a = "111";
String a1 = "111";
String b = new String("111");
//对象地址是同一个
//==比的是变量的值(值:指向内容的引用地址),equals比的是变量的内容
System.out.println(a==a1);
//对象内容是一样的
System.out.println(a.equals(a1));
//对象地址不一样
System.out.println(a==b);
//对象内容是一样的
System.out.println(a.equals(b));
}
}
结果解析
输出结果:true true false true
第一个输出true,a和a1两个变量保存的引用地址是同一个。
第二个输出true,a和a1引用地址中内容是一样的。
String a = "111"在JVM申请内存存放"111"对应的对象,当String a1="111"的时候,先去JVM里寻找是否存在"111",如果存在直接把对象的引用地址给a1。此时的a和a1都保存着同一个引用地址。String b = new String("111")创建一个对象然后把对象引用地址赋给变量b,先去JVM里找 "111",找到了直接存放引用地址。找不到创建一个对象然后把引用地址给String的有参构造方法里。所以第三个中输出false,因为a和b所保存的对象引用是不一样的。
最后一个输出true。那是因为两个变量所保存的引用地址中的内容都是“111”。
String是否有长度限制
在Java中String是有长度限制的,在JVM编译中有规范。String长度限制的场景:将某固定文件转码成Base64的形式用字符串存储,运行时需要的时候在转回来,文件比较大。String a = "ssssssss..."构造的10万个字符的字符串,编译之后虚拟机提示报错,提示字符串长度过长。字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数且String类中返回字符串长度的方法length()返回值也是int ,通过int类型对应的包装类Integer源码中可以看到其长度最大限制为2^31 -1,说明数组的长度是0~2^31-1,那么大小就是(2^31-1 = 2147483647 = 2GB)。 但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义,对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535, 但是JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错。
StringBuffer解析
StringBuffer是可变的字符序列,当一个StringBuffer被创建以后,通过StringBuffer提供append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。StringBuffer的直接父类是AbstractStringBuilder,实现了Serializable即StringBuffer的对象可以串行化,在父类中AbstractStringBuilder有属性char[] value,不是final,该value数组存放字符串内容,存放在堆中,StringBuffer是一个final 类,不能被继承,因为StringBuffer字符内容是存在char[] value所以在变化(增加/删除) 时,不用每次都更换地址(不用创建新对象)效率高于String。
/**
* @Author
* StringBuffer
**/
public class StringBuffer01 {
public static void main(String[] args) {
//创建一个大小为16的char[],用于存放字符内容
StringBuffer stringBuffer01 = new StringBuffer();
//2.通过构造器指定char[]的大小
StringBuffer stringBuffer02 = new StringBuffer(100);
//通过给一个String 创建 StringBuffer,char[] 大小就是str.length + 16
StringBuffer stringBuffer03 = new StringBuffer("hello");
String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);
System.out.println(sb.length());//4
}
}
String str = null是成立的,但是在StringBuffer的源码中append()方法:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
从源码中看出StringBuffer中的append方法调用了父类的append方法,进父类AbstractStringBuilder查看父类的append方法,源码如下:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
str为空时,调用appendNull()方法,追进appendNull()方法
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
把空对象转化为字符数组’null’,故最后输出的结果应该为4。
StringBuilder解析
StringBuilder和StringBuffer相似,两个类的构造器和方法也基本相同。不同的是StringBuffer是线程安全的,StringBuilder没有实现线程安全功能,所以性能略高。StringBuffer类中的方法都添加了synchronized关键字,给方法添加了一个锁,用来保证线程安全。Java9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。在Java9以前字符串采用char[]数组来保存字符,字符串的每个字符占2字节,Java9的字符串采用byte[]数组再加一个encoding-flag字段来保存字符,字符串的每个字符只占1字节,所以Java9的字符串更加节省空间。
StringBuilder源码
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
是什么导致了StringBuilder的线程不安全,进入父类AbstractStringBuilder
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
问题在这两行
ensureCapacityInternal(count + len);
putStringAt(count, str);
ensureCapacityInternal() 是一个扩容方法
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity) << coder);
}
}
coder是字符的编码格式,编码默认是Latin1,对应的coder是0。还有一种编码UTF16,对应的coder为1。用count(已经使用的长度)+len(要拼接的长度)得到需要的最小长度minimumCapacity。如果这个长度比原来的容量大,则触发扩容,把原数组复制到一个容量为(minimumCapacity*2+2)的新数组。在并发情况下,可能有多个线程拿到相同count,导致扩容不充分引起数组下标越界异常。一般要三个以上线程同时拿到count且必须是在程序开始时,数组不大的时候才可能出现这个异常。再来看putStringAt():
private final void putStringAt(int index, String str) {
if (getCoder() != str.coder()) {
inflate();
}
str.getBytes(value, index, coder);
}
在多线程下,可能有多个线程拿到相同count,在执行getBytes时,这几个线程添加的位置是相同的,可能会发生数据覆盖的情况。StringBuilder线程安全测试示例:
@Test
public void stringDemo02() throws InterruptedException {
CountDownLatch count=new CountDownLatch(10000);
StringBuilder stringBuilder=new StringBuilder();
for (int i = 0; i <100 ; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <100 ; j++) {
stringBuilder.append("q");
count.countDown();
}
}
});
t.start();
}
count.await();
System.out.println(stringBuilder.length());
}
9912
关键字、操作类相关
final关键字:final修饰的类叫最终类,该类不能被继承。final 修饰的方法不能被重写。final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
操作字符串的类有:String、StringBuffer、StringBuilder。String和StringBuffer、StringBuilder的区别在于String声明的是不可变的对象,每次操作都会生成新的String对象,然后将指针指向新的 String对象,而StringBuffer、StringBuilder可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用String。 StringBuffer和StringBuilder最大的区别在于StringBuffer 是线程安全的而StringBuilder是非线程安全的,StringBuilder的性能高于StringBuffer,在单线程环境下推荐使用StringBuilder,多线程环境下推荐使用StringBuffer。String str="i"与String str=new String("i")区别,String str="i"的方式,Java 虚拟机会将其分配到常量池中,String str=new String("i") 则会被分到堆内存中。
字符串反转
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer. append("abcdefg");
System. out. println(stringBuffer. reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder. append("abcdefg");
System. out. println(stringBuilder. reverse()); // gfedcba
String类的常用方法:
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
普通类和抽象类区别
普通类不能包含抽象方法,抽象类可以包含抽象方法,抽象类不能直接实例化,普通类可以直接实例化。抽象类不能使用final修饰,定义抽象类就是让其他类继承的,如果定义为final该类就不能被继承,这样彼此就会产生矛盾,所以final不能修饰抽象类。
接口和抽象类区别
抽象类的子类使用extends来继承,接口必须使用implements来实现接口。抽象类可以有构造函数,接口不能有。类可以实现很多个接口,但是只能继承一个抽象类。接口中的方法默认使用 public修饰,抽象类中的方法可以是任意访问修饰符。
泛型
泛型就是可以适应不同的类型,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。保证了类型的安全性:泛型约束了变量的类型,保证了类型的安全性。例如List和ArrayList。List集合只能加入int类型的变量,ArrayList可以Add任何常用类型,编译的时候不会提示错误。泛型能够省去类型强制转换。提高方法、算法的重用性。
//泛型类
public class GenericClass<T> {
private T value;
public GenericClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
//泛型接口
public interface GenericInterface<T> {
void show(T value);
}
//泛型方法
public class GenericFun {
public void show(String value) { }
public void show(Integer value) { }
}