Java高级-常用类

9.1.字符串相关的类
  • 理解String的不可变性

image.png

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字符串的内存分析

image.png

image.png

image.png

image.png

image.png

image.png

  • 一到面试题
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
    }
}
  • 内存分析图

image.png

  • 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));
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值