Java高级-常用类
9.1.字符串相关的类
- 理解String的不可变性
String: 字符串,使用一对""引起来表示
1.String声明为final的,不可被继承
2.String实现了Serializable接口: 表示字符串是支持序列化的.即可以把字符串变成字节流通过网络传给对方
实现了Comparable接口: 表示String可以比较大小
3.String内部定义了final char value用于底层存储字符串数据,加final表示数组不能再被重新赋值,数组的元素也不能再被修改
4.String:代表不可变的字符序列.简称: 不可变性.
体现: 1.当对字符串重新赋值时,要重新指定内存区域赋值,不能用原有的value进行赋值.
2.当对现有的字符串进行连接操作时,也要重新制定内存区域赋值,不能用原有的value进行赋值.
3.当调用String的replace()方法修改指定字符或字符串时,也要重新指定内存区域赋值.不能用原有的value进行赋值.
5.通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中.
6.字符串常量池中不会存储相同内容(可理解为用重写后的equals比较的)的字符串的
@Test
public void test1(){
// 只要对字符串的内容进行任何修改都必须重新造内存,原有的都不能动,这就叫不可变性
// 常量池中的字符串是不可变性,但可以新创建,有final的原因也有引用的原因
// value数组是final只是说明字符数组的首地址值和数组长度不能改变.
// 首次在常量池里造一个abc指向s1,这种赋值方式都认为是保存在方法区中的字符串常量池中
// 常量池中不会存两个相同内容的字符串的
String s1 = "abc";// 直接给字符串赋值,而不是new的,只有String类型是这样的,字面量的定义方式
String s2 = "abc";
// 字符串底层用value数组存的,原本s1长度指定是3,数组长度一旦确定不能再修改,又因为数组修饰为final,所以不能在原指定的数组修改值,只能新开辟空间
// 此时新的存hello的空间的地址指向s1,但此时对s2没影响(还是abc),体现不可变性:不可以在原有的位置对原有的value值重新赋值
s1 = "hello";
// s1和s2在内存中用的是同一个内容
System.out.println(s1 == s2);// 比较s1和s2的地址值
System.out.println(s1);// hello
System.out.println(s2);// abc
System.out.println("===============");
String s3 = "abc";// 一开始和s2指向同一个内存空间
s3 += "def"; // 拼接后重新指定内存区域
// 在现有的字符串后拼接新内容,要新创建一个空间存abcdef
System.out.println(s3);// abcdef
System.out.println(s2);// abc
System.out.println("================");
String s4 = "abc";
String s5 = s4.replace('a', 'm');
System.out.println(s5);//mbc, 重新造内存空间
System.out.println(s4);//abc
}
}
String的实例化方式:
方式一: 通过字面量定义的方式
方式二: 通过new + 构造器的方式
面试题: String s = new String("abc");方式创建对象,在内存中创建了几个对象?
两个: 一个是堆空间中new的结构创建的对象,另一个是char对应的常量池中的数据"abc"
如果一开始常量池里声明过了,用现有就可以了,因为常量池里不会放两个相同内容的"abc",但实际上还是有两个对象
@Test
public void test2(){
// 通过字面量定义的方式: 此时s1和s2的数据声明在方法区中的字符串常量池中
String s1 = "javaEE";
String s2 = "javaEE";
// 通过new + 构造器的方式: 此时s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值
// new的String构造器参数是对象,他就会有value这个属性,而value是char型数组final的,value属性也有个值,因为他是引用类型变量,所以存的是地址值,存的地址值是常量池中对应字符串的地址值
String s3 = new String("javaEE");// new的时候首先要在堆中加载(开辟空间)
String s4 = new String("javaEE");// 参数存的是String类型的对象,该对象是字符型数组,传入的字符串作为参数赋给了该对象的属性value
System.out.println(s1 == s2);// true
System.out.println(s1 == s3);// false
System.out.println(s1 == s4);// false
System.out.println(s3 == s4);// false
System.out.println("==============");
Person p1 = new Person("tom", 12);
Person p2 = new Person("tom", 12);
System.out.println(p1.name.equals(p2.name)); // true; name是String类类型,String类型的equals方法重写过比较的是内容
System.out.println(p1.name == p2.name);// true; 因为name通过字面量方式定义的,所以name的数据存放在字符串常量池中,两个name都记录常量池中同一个tom的地址值,因此地址值相同
p1.name = "Jerry";
System.out.println(p2.name);// tom; 不可变性,p1只能重新指向新内存区域
}
public class Person {
String name;
int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
结论:
1.常量(字面量)与常量的拼接结果在常量池.且常量池中不会存在相同内容的常量.
2.只要其中有一个是常量,结果就在堆中.
3.如果拼接的结果调intern()方法,返回值就在常量池中
@Test
public void test4(){
String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
final String s4 = "javaEE";
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);// true 因为s4加了final修饰,变成常量了
}
@Test
public void test3(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";//s3和s4一样所以就一份
String s4 = "javaEE" + "hadoop";// 通过字面量方式定义,看成两个字面量的连接的方式,相当于就是s3,两个内容一样所以就在常量池中声明
// 赋值时候,其中只要有变量名参与,此时都不在常量池,因为常量池中相同内容只能有一份,而都得在堆空间开辟,就相当于new
// 以s5为例: 在栈中声明一个变量s5,首先在堆空间中new一个对象,该对象的地址值就赋给了s5,然后赋值的字符串作为字面量本质上数据在常量池中,堆空间中的对象的属性(value)作为引用变量存放地址值指向常量池中的字面量
// 所以s5,s6,s7记录的的堆空间的地址值所以都不一样
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);// true;s3和s4一样所以就一份
System.out.println(s3 == s5);// false
System.out.println(s3 == s6);// false
System.out.println(s3 == s7);// false
System.out.println(s5 == s6);// false
// s8存的是s5对象在常量池存的地址
String s8 = s5.intern();// intern()是字符串中的方法,通过字符串对象调该方法的时候,不管对象是在堆还是在其他地方,intern方法的返回值强制要求在常量池中声明
System.out.println(s3 == s8);// true
String s9 = (s1 + s2).intern();
System.out.println(s3 == s9);// true;如果拼接的结果调用intern方法,返回值就在常量池
}
- String字符串的内存分析
- 一到面试题
public class StringTest {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't' };
public void change(String str, char[] ch){
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StringTest ex = new StringTest();
ex.change(ex.str, ex.ch);
System.out.println(ex.str);// good;由于字符串的不可变性,change方法中的局部变量test ok不会覆盖掉常量池中的good,而是重新在常量池里开辟新空间
System.out.println(ex.ch); // best
}
}
- 内存分析图
- JVM中涉及到字符串的内存结构
- String的常用方法
public class StringMethodTest {
@Test
public void test3(){
String s1 = "helloworld";
boolean b1 = s1.endsWith("ld");// 测试此字符串是否以指定后缀结束
System.out.println(b1);
boolean b2 = s1.startsWith("He");// 测试此字符串是否以指定前缀开始
System.out.println(b2);
boolean b3 = s1.startsWith("ll",2);// 测试此字符串是否以指定索引值的前缀开始
System.out.println(b3);
String s2 = "wo";
System.out.println(s1.contains(s2));// 判断字符串是否包含指定字符串,类似于KMP算法
System.out.println(s1.indexOf("lo"));// 判断当前字符串在指定字符串第一次出现的索引值,找不到返回-1
System.out.println(s1.indexOf("lo", 5));// 从指定索引值开始找指定字符串
String s3 = "hellorworld";
System.out.println(s3.lastIndexOf("or"));// 从后往前找指定字符串,但还是从前往后数
System.out.println(s3.lastIndexOf("or", 6));// 从指定索引值开始从后往前找指定字符串,从前往后数
/*
什么情况下,indexOf(str)和lastIndexOf(str)返回值相同?
情况一: 存在唯一的一个(单个)str.情况二: 不存在str,返回-1
*/
}
@Test
public void test2(){
String s1 = "HelloWorld";
String s2 = "helloworld";
System.out.println(s1.equals(s2));
System.out.println(s1.equalsIgnoreCase(s2));// 忽略大小写比较字符串实体内容,用于验证码
String s3 = "abc";
String s4 = s3.concat("def");// 将指定字符串连接到字符串末尾,等同于"+",用于数据库
System.out.println(s4);
String s5 = "abc";
String s6 = new String("abd");
System.out.println(s5.compareTo(s6));// 比较两个字符串大小,99-101=-2,负数:前者大,正数:后者大;涉及字符串排序
String s7 = "超人打怪兽";
String s8 = s7.substring(2);// 返回一个新字符串,此字符串从开始索引位置开始截取一个子字符串
System.out.println(s7);// 不变
System.out.println(s8);// 打怪兽
String s9 = s7.substring(2,4);// 从开始索引位置(包含)到结束索引位置(不包含)截取一个字符串,左闭右开区间
System.out.println(s9);
}
@Test
public void test1(){
String s1 = "HelloWorld";
System.out.println(s1.length());// 10; 底层数组的长度
System.out.println(s1.charAt(0));// h; 返回某索引处的字符,本质也是操作数组
System.out.println(s1.charAt(9));// d
// System.out.println(s1.charAt(10));
System.out.println(s1.isEmpty());// false;判断字符串(底层数组的长度是否为0)是否为空
// 转换或匹配要用到,本身字符串不变
String s2 = s1.toLowerCase();// 将String类型的字符全部转换成小写
System.out.println(s1); // 体现s1不可变性,转换方法没有对s1本身修改
System.out.println(s2); // 新造一个内存区域赋给s2
String s3 = " he llo world ";
String s4 = s3.trim();// 去除字符串首尾空格,字符串内的不变,用于登录注册
System.out.println(s4);
}
}
- String与基本数据类型,包装类之间的转换
String与基本数据类型,包装类之间的转换
String --> 基本数据类型,包装类: 调用包装类的静态方法: parseXxx(str)
基本数据类型,包装类 --> String: 调用String重载的vallueOf(xxx)
@Test
public void test1(){
String s1 = "123";// 在常量池中
int n1 = Integer.parseInt(s1);
String s2 = String.valueOf(n1);// "123"
String s3 = n1 + ""; // 只要有变量参与都在堆中
System.out.println(s1 == s3);// false ;
}
- String 与 char[] 之间的转换
String --> char[]: 调用String的toCharArray()
char[] --> String: 调用String的构造器
@Test
public void test2(){
String s1 = "abc123";
char[] charArray = s1.toCharArray();
for (int i = 0; i < charArray.length;i++){
System.out.println(charArray[i]);
}
char[] arr = new char[]{'h','e','l'};
String s2 = new String(arr);
System.out.println(s2);
}
- String 与 byte[]之间的转换
编码: String --> byte[]: 调用String的getBytes()
解码: byte[] --> String: 调用String的构造器
编码: 字符集 --> 字节 (看得懂的 --> 看不懂的二进制数据)
解码: 编码的逆过程,字节 --> 字符串(看不懂的二进制数据 --> 看得懂)
说明: 解码时,要求解码使用的字符集必须与编码时用的字符集一致,否则会出现乱码
@Test
public void test3() throws UnsupportedEncodingException {
String s1 = "abc123中国";//使用默认字符集(UTF-8),进行编码;中文不存在ASCII值中,因为当前使用UTF-8字符集,一个汉字占三位
byte[] bytes = s1.getBytes();
System.out.println(Arrays.toString(bytes));// 遍历数组
byte[] gbks = s1.getBytes("gbk");// 用gbk字符集编码;用另一种支持中文的字符集
System.out.println(Arrays.toString(gbks));
System.out.println("======解码========");
String s2 = new String(bytes);//没有指定,还是用默认字符集解码
System.out.println(s2);
String s3 = new String(gbks); // 解码用了UTF-8所以会乱码
System.out.println(s3);// 乱码; 编码集和解码集不一致
String s4 = new String(gbks,"gbk");//编码集和解码一致
System.out.println(s4);
}
- StringBuffer和StringBuilder的介绍
String,StringBuffer,StringBuilder三者的异同?
String: 不可变的字符序列;底层使用char[]存储,效率最低,每次都需要重造
StringBuffer: 可变的字符序列: 线程安全的,效率低;底层使用char[]存储
StringBuilder: 可变的字符序列: jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储
- String,StringBuffer,StringBuilder源码分析:
String str = new String();//char[] value = new char[0];
String st1 = new String("abc");//char[] value = new char[]{'a','b','c'};
StringBuffer sb1 = new StringBuffer();// char[] value = new char[16];底层创建了一个长度为16的数组
可在原数组修改,体现可变性
sb1.append('a');// value[0] = 'a';
sb1.append('b');// value[1] = 'b';
StringBuffer s2 = new StringBuffer("abc");// char[] value = new char["abc".length() + 16];
问题一: System.out.println(s2.length());// 3
问题二: 扩容问题: 如果要添加的数据底层数组放不下了,那就要扩容底层数组.
默认情况下,扩容为原来容量的2倍 + 2,同时将原有数组中的元素复制到新的数组中.
指导意义: 开发中建议使用: StringBuffer(int capacity)或StringBuilder(int capacity)为了避免扩容,一开始就用带参数的构造器,这样效率才高
@Test
public void test1(){
StringBuffer s1 = new StringBuffer("abc");
s1.setCharAt(0, 'm');
System.out.println(s1);
StringBuffer s2 = new StringBuffer();
System.out.println(s2.length());// 0,根据length方法,返回值是数组中元素的个数
}
- StringBuffer,StringBuilder的常用方法
@Test
public void test2(){
StringBuffer s1 = new StringBuffer("abc");
s1.append(1);// 添加元素,用于字符串拼接
s1.append("1");
// System.out.println(s1);// abc11
//s1.delete(2,4);// ab1 删除指定位置的内容,左闭右开
// s1.replace(2, 4, "hello");// 把[start,end)位置替换为str
//s1.insert(2, "false");// 指定索引值插入元素,和String连接符一样,把false看成5个字符
//s1.reverse();// 把当前字符序列反转
String s2 = s1.substring(1,3);//返回指定索引位置的字符串,没有把原字符串切割
System.out.println(s1.charAt(0));//查找
System.out.println(s2);
System.out.println(s1.length());
System.out.println(s1);// 可变的
}
/*
String,StringBuffer,StringBuilder三者的异同?
String: 不可变的字符序列;底层使用char[]存储,效率最低,每次都需要重造
StringBuffer: 可变的字符序列: 线程安全的,效率低;底层使用char[]存储
StringBuilder: 可变的字符序列: jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储
源码分析:
String str = new String();//char[] value = new char[0];
String st1 = new String("abc");//char[] value = new char[]{'a','b','c'};
StringBuffer sb1 = new StringBuffer();// char[] value = new char[16];底层创建了一个长度为16的数组
可在原数组修改,体现可变性
sb1.append('a');// value[0] = 'a';
sb1.append('b');// value[1] = 'b';
StringBuffer s2 = new StringBuffer("abc");// char[] value = new char["abc".length() + 16];
问题一: System.out.println(s2.length());// 3
问题二: 扩容问题: 如果要添加的数据底层数组放不下了,那就要扩容底层数组.
默认情况下,扩容为原来容量的2倍 + 2,同时将原有数组中的元素复制到新的数组中.
指导意义: 开发中建议使用: StringBuffer(int capacity)或StringBuilder(int capacity)为了避免扩容,一开始就用带参数的构造器,这样效率才高
*/
@Test
public void test1(){
StringBuffer s1 = new StringBuffer("abc");
s1.setCharAt(0, 'm');
System.out.println(s1);
StringBuffer s2 = new StringBuffer();
System.out.println(s2.length());// 0,根据length方法,返回值是数组中元素的个数
}
- String,StringBuffer,StringBuilder三者的效率对比:
从高到低排列: StringBuilder> StringBuffer>String
9.4.Java比较器
一.说明: Java中的对象,正常情况下,只能进行比较: == 或 != .不能用 > 或 < 的
但是在开发场景中,需要对多个对象进行排序,言外之意,就要比较对象的大小.
如何实现? 用两个接口中的任何一个: Comparable 或 Comparator 两个定义比较大小的规范
二. Comparable接口与Comparator的使用对比:
Comparable接口的方式一旦指定,保证Comparable接口实现类的对象在任何位置都可以比较大小
Comparator接口属于临时性的比较,需要的时候指定一下,临时的创建一个Comparator接口实现类去比较
- 自然排序: java.lang.Comparable
Comparable接口的使用举例: 自然排序: 默认时候会考虑Comparable,让排序这个类数据实现Comparable接口
1.像String,包装类等已经实现了Comparable接口,重写了CompareTo(obj)方法,给出了比较两个对象大小的方式
2.像String,包装类重写compareTo()方法以后,进行了从小到大的排列,所以可以直接用排序方法
3.重写compareTo(obj)方法的规则:
如果当前对象this大于形参对象obj,则返回正整数,
如果当前对象this小于形参对象obj,则返回负整数,
如果当前对象this等于形参对象obj,则返回零
4.对于自定义类来说,如果要排序,可以让自定义类实现Comparable接口,重写compareTo(obj)方法.
在compareTo(obj)方法中指明如何排序
@Test
public void test1(){
String[] arr = new String[]{"AA", "CC", "KK", "MM", "GG", "JJ", "DD"};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
@Test
public void test2(){
Goods[] arr = new Goods[5];
arr[0] = new Goods("lenovomouse", 20);
arr[1] = new Goods("dell", 15);
arr[2] = new Goods("xiaomi", 43);
arr[3] = new Goods("huawei", 38);
arr[4] = new Goods("microsoft", 38);
// 调用sort方法的时候调用了Comparable接口内部的compareTo方法
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
// 实现Comparable接口
public class Goods implements Comparable{
private String name;
private double price;
public Goods() {
}
public Goods(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
// 指明商品比较大小的方法
@Override
public int compareTo(Object o) {
// 判断对象是不是个商品
if (o instanceof Goods){
Goods goods = (Goods) o;
// 方式一:
if (this.price > goods.price){
return 1;
}else if (this.price < goods.price){
return -1;
}else{
// 二级排序: 当价格一样时,按名字比较,如果又是自定义类,则要在自定义类中再重写compareTo方法
return this.name.compareTo(goods.name);
}
// 方式二: 使用包装类的compare方法
//return Double.compare(this.price,goods.price);
}
// 类型错误,抛运行时异常
throw new RuntimeException("传入数据类型不一致");
}
}
- 定制排序: java.util.Comparator
Comparator接口的使用: 定制排序
1.背景:
当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,
或实现了java.langComparable接口的排序规则不适合当前的操作,
那么可以考虑使用Comparator的对象来排序
2.重写Compare(Object o1,Object o2)方法,比较o1和o2的大小:
如果返回0,表示相等;
返回负整数,表示o1小于o2.
// String类型实现Comparator接口的排序方法
@Test
public void test3(){
String[] arr = new String[]{"AA", "CC", "KK", "MM", "GG", "JJ", "DD"};
// 如果只传一个参数,则是从小到大排序
// 传两个参数,用Comparator接口的匿名实现类的匿名对象
Arrays.sort(arr, new Comparator() {
// 重写Comparator接口中的compare方法
// 其他方法是静态的,不能被重写
// 定制排序: 按照字符串从大到小排列
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof String && o2 instanceof String){
String s1 = (String) o1;
String s2 = (String) o2;
// 可以调用String类重写过的compareTo方法比较: 默认从大到小排
return -s1.compareTo(s2);
}
// 类型不一致,抛异常
throw new RuntimeException("输入类型不一致");
}
});
System.out.println(Arrays.toString(arr));
}
// 自定义类型实现Comparator接口的排序方法
@Test
public void test4(){
Goods[] arr = new Goods[6];
arr[0] = new Goods("lenovomouse", 20);
arr[1] = new Goods("dell", 15);
arr[2] = new Goods("xiaomi", 43);
arr[3] = new Goods("huawei", 38);
arr[4] = new Goods("huawei", 338);
arr[5] = new Goods("microsoft", 38);
Arrays.sort(arr, new Comparator() {
// 指明商品比较大小方式: 先按照商品名称从低到高排序,再按照价格从高到低排序
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Goods && o2 instanceof Goods){
Goods g1 = (Goods) o1;
Goods g2 = (Goods) o2;
// 先判断名字是否相同
if (g1.getName().equals(g2.getName())){ // 名字相同,比较价格,用Double类重写的compare方法
return -Double.compare(g1.getPrice(), g2.getPrice());
}else{ // 名字不同,按名字从低到高排,用String类型重写过的compareTo方法比较
return g1.getName().compareTo(g2.getName());
}
}
// 类型不同,抛异常
throw new RuntimeException("输入类型不一致");
}
});
System.out.println(Arrays.toString(arr));
}