Day8 trim方法String的偏底层字符串常量池intern方法装箱拆箱集合的ArrayList,LinkedListStringBuilder/Buffer
感觉自己学的有点多了,前面的知识没太复习,写代码的时候都得回头看看一些方法是谁的。我的妈呀。还是得多回头看。
先复盘吧
trim方法
String字符串的方法
删去字符串前后位置中的空白位。
什么是空白位:空格,\n,\r,\f,\t这些都是空白位
//trim() //去除字符串前后位置的空白位 \r 回车 \n 换行 \t 制表符 \f 换页都算空白位 str = "\r \n a b c \t \f"; //\r 回车 \n 换行 \t System.out.println(str); System.out.println("-------------"); String result = str.trim(); System.out.println(result);这个运行结果输出就是a b c
再复习复习String字符串的其他方法
split indexOf/LastIndexOf CharAt replace/replaceAll length substring toCharArray equals
String的偏底层
String的类定义中
他的字符串的真实值其实是用final修饰的,即String实例化出的对象内容不可以改变(String对象是不可变对象、不可变量),那么String的引用修改内部内容时是如何作用的?
其实是生成了一个新的String对象,然后引用再指向这个新的对象。
另一个问题就出现了,你要是这样的话是不是就会产生大量的内存空间浪费,也可能导致内存溢出(OOM),所以为了解决这个问题就整出来了个字符串缓冲池和可变字符串StringBuilder和StringBuffer。
字符串常量池
常量池在堆中
存储通过值来声明的字符串String对象
目的:
为了能够达到减少内存消耗,对象重用的效果,Java中提供了字符串常量池,减少字符串定义的消耗
工作原理:
当Java程序需要定义(创建)一个字符串,不会直接开辟空间,而是先到字符串常量池中查阅查看是否有对应的字符串内容(副本),如果有,则直接使用字符串常量池中的对象。如果没有的话,在常量池创建一个副本,然后再使用此副本
注:不是任何创建或定义字符串的方法都会存入缓冲池,只有使用值的方式声明的字符串才会进入常量池,即让"1234"作为一个String字符串对象进入常量池
具体代码
String a = "1234"; String a1 = "1234";1234就是一个String对象,通过值的方式声明,存储在缓冲池,如果其他String引用也通过值这种方式声明,如a1,也会将缓冲池中的"1234"String对象地址给a1,所以a==a1是成立的。
而如果是new的会先去常量池查找有没有这个String对象,如果有,就会在堆中开辟一个新的空间,将常量池中的String对象的值赋给新实例化的对象,并将这个新的对象的地址给引用。如果常量池中没有,就会在常量池中先实例化这个对象,new出来的String对象的值会指向常量池中这个对象的值,下面是代码解释
//并不是所有的字符串都会加入字符串常量池 //只要是new都是一个新的独立的内容 String s1 = "1234"; String s2 = "1234"; String s3 = new String("1234"); //1234是作为值来参与new String的初始化函数的,他会先去常量池去找有没有1234这个对象,如果有就创建一个新的对象 // 这个对象全部指向常量池中的对象的值,如果常量池中没有,那就先在常量池中先创建这个对象,然后在堆中 //声明的对象会指向常量池中的对象的值 new String("1234"); //这个语句执行会创建一个或两个对象所以,new String("1234");可能会创建一个或两个对象
StringBuilder/Buffer
字符串拼接时,或使用+拼接时,每次拼接都会产生一个新的字符串 ,当需要连续多次拼接时,会产生大量的中间串,会消耗大量内存 ,为了避免产生中间串,java提供了可变字符串类型,可变长度,不是final private了
这是优化内存空间的另一个方法,这两个是可变字符串数组类,其中的值不是用final修饰的,可以对原有的字符串数组进行扩展和延申,不过扩展延申也是重新开辟了一个空间,只是old和新的开辟的空间大小不同。
他们默认开辟的空间大小为16,后面扩容每次扩为原来的二倍。
这两个是可变数组,但是也是可以转为正常数组的。
String result = ""; for(int i = 0;i<100;i++){ result = result+i; } System.out.println(result); //字符串拼接时,或使用+拼接时,每次拼接都会产生一个新的字符串 //当需要连续多次拼接时,会产生大量的中间串,会消耗大量内存 //为了避免产生中间串,java提供了可变字符串类型,可变长度,不是final private了 //StringBuilder StringBuffer StringBuilder stringBuilder = new StringBuilder(); stringBuilder = new StringBuilder(20);//修改容量 stringBuilder = new StringBuilder('a');//将a当作一个容量去存储的,a前面是capacity,容量为a的ACII编码值 stringBuilder = new StringBuilder("123");//将123这个字符串放里面,不是声明容量,和声明容量方法不同 for(int i = 0;i<100;i++){ stringBuilder.append(i); } //StringBuilder 和StringBuffer都是可变字符串, /// 为什么会有两个,一个是线程安全 Builder安全,一个是线程不安全 //内部提供了一个可变化的value数组 //默认初始容量为16,,每次扩容扩两倍 result = stringBuilder.toString(); System.out.println(result); System.out.println(stringBuilder.length()); //这个就不会产生中间字符串对象,因为他是可变字符数组String的常量编译优化问题以及值String对象和newString对象的拼接
String s1 = "1234"; String s2 = "1234"; String s3 = new String("1234"); //1234是作为值来参与new String的初始化函数的,他会先去常量池去找有没有1234这个对象,如果有就创建一个新的对象 // 这个对象全部指向常量池中的对象的值,如果常量池中没有,那就先在常量池中先创建这个对象,然后在堆中 //声明的对象会指向常量池中的对象的值 new String("1234"); //这个语句执行会创建一个或两个对象 String s4 = new String("1234"); String s5 ="12"+"34";//编译代码时已经将12 34拼接完成,运行代码时,就是s5="1234" String s6 ="1"+"2"+"3"+"4"; String s7 ="12"+new String("34");//字符串只要不是编译时就能确定的结果(需要在运行时拼接) // 那就会创建一个新的对象,让引用指向他 String s7a ="12"+new String("34"); String s8a ="12"; String s8b ="34"; String s8 = s8a+s8b; String s9 = s8a+s8b; ///常量编译优化----------得了解 System.out.println(s1==s5);//T/F ------true System.out.println(s1==s2);//true System.out.println(s1==s3);//false System.out.println(s3==s4);//false System.out.println(s1==s6);//常量编译优化true System.out.println(s5==s6);//常量编译优化true System.out.println(s1==s7);//false, System.out.println(s3==s7);//false System.out.println(s7==s7a);//false System.out.println(s1==s8);//false System.out.println(s8==s9);//false只要是值操作,在编译的时候就可以确定拼接后的结果。如果有String开辟新空间或者引用拼接,那就会创建一个新对象,让引用指向他
intern方法
并不是所有字符串都会加入常量池
String str = new String(new char[]{'1','2','3'});这个就不会在常量池生成对象。那么如何让他进入常量池,即使用intern方法
intern的使用方法:
String str = new String(new char[]{'1','2','3'}); String str1 = str.intern();intern方法的作用过程:先去常量池寻找有没有这个String对象,如果有,则返回这个对象的地址,如果没有,则在常量池创建对象,并返回地址
注:当两个字符串的equals比较为true,两个字符串的inter方法==比较一定为true
装箱&拆箱
Java是一个面向对象的语言,但是也有非对象的数据类型,比如基本数据类型
为了圆这个坑,就创建了基本数据类型类,即封装类,基本数据类型和封装类间可以转换
//byte short int long char float double boolean //封装类型Byte Short Integer Long Character Float Double Boolean //整数和浮点数类型的封装类都是继承自Number,超级父类是Number所有的封装类都有一个超级父类,Number类
装箱:基本数据类型转为对应的封装类
拆箱:封装类转为对应的基本数据类型
//基本数据类型和对应的封装类型可以相互转换 byte b = 12; Byte b1 = 12; b =new Byte("12");//JDK9后就废弃了这个方法,但是不影响 b = Byte.valueOf("12"); //基本数据类型转为对应封装类型叫做装箱 //基本数据类型的封装类型转为基本数据类型这个过程叫做拆箱 Integer a = 12;//装箱 int c = new Integer(12);//拆箱封装类型的缓存
机理类似于常量池,封装类型也有自己的常用池,整数类型的封装类型,有缓存,但不是所有的数都有缓存,范围是从-128~127,且Integer封装类型的缓存可以通过修改JVM的参数来修改缓存大小,boolean也有缓存,因为就true和fales。
Integer i1 = 12; Integer i2 = new Integer(12); // Integer i2 = Integer.valueOf("15"); System.out.println(i1==i2);//false Integer i3 = 12; System.out.println(i1==i3);//true Integer i4 = -128; Integer i5 = -128; System.out.println(i4==i5);//true i4 = 128; i5 = 128; System.out.println(i4==i5);//false Character c1 = 'a'; Character c2 = 'a'; System.out.println(c1==c2);//true int numa = 1000; Integer numb=1000; //基本数据类型和引用装箱类型==比较,将引用类型拆箱后再比较,和缓存就没有关系了 System.out.println(numa==numb);//true超出了缓存范围就会创建一个对应类型的对象。
ArrayList&LinkedList
这个先从实现类说起,慢慢说到集合
集合(Collection)
集合是一个接口
有三类容器,List Set Map
其中List Set Map
List下有三个实现类:ArrayList LinkedList Vector
List和Set有一个共同的父接口Collection
ArrayList
ArrayList,顾名思义,数组链表,通过动态数组保存数据,使用数组实现链接
private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; // non-private to simplify nested class access private int size;查看ArrayList的源码,发现他的elementData;并不是final修饰,size也不是final修饰,这就说明他是一个可变动态数组,且初始默认大小是10。
每个节点可以存储任何类型的对象
初始大小为10,每次扩容变为原来的1.5倍
每个下标都是一个对象,对象可以是任意类型,他们的超父类是Object类
所有的List都是有序集合,存入是什么顺序就是什么顺序了(后面有无序的
然后再说一下ArrayList中的方法:
1>list1引用名.add()
向链表中加入一个数据对象,如果是基本数据类型则会进行自动的装箱操作,然后保存对应的封装对象。也可以向其中存null值(有不可以存null值的,因为后面有的容器需要比较hash值进行无序存储,比如TreeSet,大部分的Map)
2>list1.set(下标位置,对象或值)
3>list1.remove(下标位置)
4>list1.get(下标位置)//返回下标中的对象
5>list1.size()返回数组大小
LinkedList
LinkedList使用链表实现的,还是用双向链表实现的,每个链表都是由结点构成,方法和ArrayList实现方法相同,但是底层实现方式不相同。所以ArrayList和LinkedList在功能上的性能会有差距。
ArrayList查找快,增删慢
LinkedList查找慢,增删快
因为ArrayList可以通过下标定位数组位置,直接找到对应对象,所以查找快,但是增删的话需要将后面的所有对象都移动,所以增删慢。
而LinkedList查找需要从头节点或者尾结点开始遍历查找,所以查找慢,但是删除的话只需要改变他们的fore指针和next指针就可以实现结点的删除或增加,所以增删快。
为什么要用双向链表?
因为这样检索的效率会提升。可以从两个方向查找数据,依据下标所在位置,分别从前或后找到对应位置,加快检索效率,他查找数据可以从两个方向查找。
注:
List可以存储重复的值以及null值。
List的遍历:
使用foreach方法
for(Object obj:list){
代码块
}
