美文网首页
Java高级-常用类

Java高级-常用类

作者: ttiga | 来源:发表于2021-05-06 13:44 被阅读0次

    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));
        }
    

    相关文章

      网友评论

          本文标题:Java高级-常用类

          本文链接:https://www.haomeiwen.com/subject/mqntdltx.html