第四章 数组

数组概述

  • 什么是数组?
    • 在java中,数组是一种用于存储多个相同数据类型的容器
    • 例如一个存储整数的数组: int[] nums = {100,200,300};
    • 例如一个存储字符串的数组: String[] names = {“jack” , “lucky” , “lisa”};
    • 数组是一种引用数据类型,隐式继承Object.因此数组也可以调用Object类中的方法.
    • 数组对象存储在堆内存当中
  • 数组的分类?
    • 根据维数进行分类: 一维数组,二维数组,三维数组,多维数组
    • 根据数组中存储元素的类型分类:基本类型数组,引用类型数组
    • 根据数组初始化方式不同分类:静态数组,动态数组
  • java数组存储元素的特点?
    • 数组长度一旦确定不可变
    • 数组中元素类型一致,每个元素占用空间大小相同
    • 数组中每个元素在空间存储上,内存地址是连续的.
    • 每个元素有索引,首元素索引0,以1递增
    • 以首元素的内存地址作为数组对象在堆内存中的地址
    • 所有数组对象都有length属性用来获取数组元素个数,末尾元素下标: length-1.

数组的优缺点

  • 数组的优点?
    • 根据下标查询某个元素的效率极高.数组中有100个元素和有100万个元素,查询效率相同.时间复杂度O(1).也就是说在数组中根据下标查询某个元素时,不管数组的长短,耗费时间是固定不变的.
    • 原因:知道首元素内存地址,元素在空间存储上内存地址又是连续的,每个元素占用空间大小相同,只要知道下标,就可以通过数学表达式计算出要查找元素的内存地址.直接通过内存地址定位元素
  • 数组的缺点?
    • 随机增删元素的效率低,因为随机增删元素时,为了保证数组中元素的内存地址连续,就需要涉及到后续元素的位移问题.时间复杂度O(n).O(n)表示的线性阶,随着问题规模n的不断扩大,算法的执行效率越低.(不过要注意的是:对数组末尾元素的增删效率是不受影响的)
    • 无法存储大量数据,因为很难在内存上找到非常大的一块连续的内存

一维数组

package com.powernode.javase;

/**
 * 1. 静态初始化一维数组:
 *      第一种方式:
 *      数据类型[] 变量名 = new 数据类型[]{元素1,2,3...}
 *      比如: int[] arr = new int[]{元素1,2,3...}
 *
 *      第二种方式:
 *      数据类型[] 变量名 = {元素1,2,3...}
 *      比如: int[] arr = {元素1,2,3...}
 *
 */
public class ArrayTest01 {
    public static void main(String[] args) {
        //静态初始化一维数组的第一种方式
        int[] arr = new int[] {100,200,300};
        String[] name = new String[]{"jack", "lucy", "tom"};

        //静态初始化一维数组的第二种方
        int[] nums = {100,200,300};
        String[] citys = {"晋中", "太原", "运城"};

        //读nums中的每一个元素
        System.out.println(nums[0]);
        System.out.println(nums[1]);
        System.out.println(nums[2]);

        //将nums数组中每个盒子中的数据修改一下
        nums[0] = 110;
        nums[1] = 210;
        nums[2] = 310;
        System.out.println(nums[0]);
        System.out.println(nums[1]);
        System.out.println(nums[2]);

        /*
        总结下我们到现在为止接触的哪些异常
            java.lang.NullPointerException
            java.lang.ClassCastException
            java.lang.ArrayIndexOutOfBoundsException
         */
        //System.out.println(nums[3]);//ArrayIndexOutOfBoundsException  数组下标越界异常

        System.out.println("nums中元素个数是" + nums.length + "个");
        System.out.println("末尾元素: " + nums[nums.length - 1]);

        //遍历citys数组
        for (int i = 0; i < citys.length; i++){
            System.out.println(citys[i]);
        }
        //从末尾开始遍历
        for(int i = citys.length - 1; i >= 0; i--){
            System.out.println(citys[i]);
        }
    }
}

for each增强for循环

package com.powernode.javase;

/**
 * 增强for循环/for-each循环.JDK5的新特性
 *
 * for each语法结构:
 *      for(数组中元素的数据类型  变量名 :  数组名){
 *          System.out.println(变量名)
 *      }
 *      注意: 变量名  代表数组中的每个元素
 *
 * for each的优点:代码简洁,可读性强
 *
 * for each的缺点:没有下标(如果需求中需要使用到下标,这种方式就差一点)
 */
public class ArrayTest02 {
    public static void main(String[] args) {
        //静态初始化一维数组
        int[] arr = {100,200,300};
        String[] names = {"jack","tom","lisa"};

        //遍历arr数组(for each)
        for(int num : arr){
            System.out.println(num);
        }

        //需求:遍历数组arr,最后一个元素要求翻倍
        for(int i = 0; i < arr.length; i++){
            if(i == arr.length - 1){
                System.out.println(arr[i] * 2);
            }else {
            System.out.println(arr[i]);
            }
        }

        //遍历names数组(for each)
        for (String name : names){
            System.out.println(name);
        }
    }
}

一维数组的动态初始化

package com.powernode.javase;

import java.util.Random;

/**
 * 一维数组的动态初始化:
 *      1. 什么时候使用动态初始化一维数组呢?
 *          当创建数组时,不知道数组中具体存储哪些元素,可以使用动态初始化
 *      2.语法格式
 *          数据类型[]  变量名  = new  数据类型[长度];
 *      3. 动态初始化一维数组之后,数组长度确定,数组中存储的每个元素将采用默认值.
 *      4. 默认值
 *              数据类型                                默认值
 *              ==============================================
 *              byte                                     0
 *              short                                    0
 *              int                                      0
 *              long                                     0L
 *              float                                    0.0F
 *              double                                   0.0
 *              boolean                                  false
 *              char                                     /u0000
 *              引用数据类型                               null
 */
public class ArrayTest04 {
    public static void main(String[] args) {
        Object[] objs = new Object[5];
        objs[2] = new Object();
        for (Object obj : objs){
            System.out.println(obj);
        }

        int[] nums = new int[10];
        //创建一个随机数生成器对象
        Random random = new Random();
        for (int i = 0; i < 10; i++){
            nums[i] = random.nextInt(101);
            System.out.println(nums[i]);
        }
    }
}

一维数组中存储引用

package com.powernode.javase.oo;

public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird();
        Cat cat = new Cat();

        //创建宠物数组,既可以存储cat,又可以存储bird
        Animal[] animals = {bird,cat,new Bird(),new Cat()};

        //遍历数组,让猫爪老鼠,鸟儿去飞翔
        for (Animal animal : animals){
            if (animal instanceof Cat){
                Cat c = (Cat) animal;
                c.catchMouse();
            }else if (animal instanceof Bird){
                Bird b = (Bird) animal;
                b.fly();
            }
        }
    }
}

查找数组中的最大值以及下标

package com.powernode.javase;

/**
 * 找数组中的最大值
 */
public class ArrayTest05 {
    public static void main(String[] args) {
        int[] arr = {10,20,30,40,5060,7000,80,80};
        int max = searchMax(arr);
        System.out.println("最大值是: " + max);
        int maxIndex = searchMaxIndex(arr);
        System.out.println("最大值下标是: " + maxIndex);
    }

    /**
     * 找最大值
     * @param arr
     * @return
     */
    public static int searchMax(int[] arr){
        int max = arr[0];
        for (int num : arr){
            if (max < num){
                max = num;
            }
        }
        return max;
    }

    /**
     * 找最大值下标
     * @param arr
     * @return
     */
    public static int searchMaxIndex(int[] arr){
        int maxIndex = 0;
        for (int i = 0; i < arr.length; i++){
            if (arr[i] > arr[maxIndex]){
                maxIndex = i;
            }
        }
        return maxIndex;
    }
}

找一个固定值在数组中的位置:遍历等于,然后输出下标

数组反转

package com.powernode.javase;

/**
 * 数组的反转:
 *      第一种方式: 创建一个新的数组
 *      第二种方式: 首尾交换
 */
public class ArrayTest06 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6,7,8,9};
        int[] newArr = reverse(arr);
        for (int num : newArr){
            System.out.println(num);
        }
    }

   /* public static void reverse(int[] arr){
        int[] newArr = new int[arr.length];
        for (int i = 0; i < arr.length; i++){
            newArr[i] = arr[arr.length - 1 - i];
        }
        for (int i = 0; i < newArr.length; i++){
            arr[i] = newArr[i];
        }
    }*/

    public static int[] reverse(int[] arr){
        int[] newArr = new int[arr.length];
        for(int i = 0; i < arr.length; i++){
            newArr[i] = arr[arr.length - 1 - i];
        }
        return newArr;
    }
}

package com.powernode.javase;

//首尾交换的方式完成数组反转
public class ArrayTest07 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};
        reverse(arr);
        for(int num : arr){
            System.out.println(num);
        }
    }

    //反转

    /**
     * 这种方式完成的数组反转,不但效率高(循环次数少),而且还节省空间,因为不需要new新的数组对象
     * @param arr
     */
    public static void reverse(int[] arr){
        //首尾交换
        //首arr[i]
        //尾arr[arr.length - 1 - i]
        for(int i = 0; i < arr.length / 2; i++){
            int temp = arr[i];
            arr[i] = arr[arr.length - 1 - i];
            arr[arr.length - 1 - i] = temp;
        }
    }
}

关于main()方法中args[]的作用

package com.powernode.javase;

/**
 *关于main方法上的String[] args 作用是什么?
 *  1. 作用: 接收命令行参数用的.
 *
 *  2. JVM负责调用ArrayTest08.main()方法.
 *      JVM负责给main方法准备一个String[]一维数组的对象
 *
 *  3. java ArrayTest08  abc def xyz
 *      底层JVM是怎么做的?
 *          命令行参数: "abc def xyz"
 *          JVM会将以上字符串以"空格"进行拆分,生成一个新的数组对象.
 *          最后这个数组对象是: String[] args = {"abc","def","xyz"};
 *
 *  4. 命令行参数有什么用?
 *      需求: 使用该系统的时候,需要提供正确的口令(用户名和密码)
 *      如果用户名是admin,并且密码是admin123,则表示合法用户,其他用户是非法的.
 *      非法用户则直接退出系统
 */
public class ArrayTest08 {
    public static void main(String[] args) {
        System.out.println(args[0]);
        System.out.println(args[1]);
    }
}
//在编译器右上角运行键旁边的Edit Configurations里面的program arguments

方法的可变长度参数

package com.powernode.javase;

/**
 * 关于方法的可变长度参数
 *      1. 语法格式:
 *          数据类型...
 *      2. 在形参列表中可变长参数只能有一个,并且只能在参数列表的末尾出现
 *      3. 可变长度参数可以当做数组看待
 */
public class ArrayTest09 {
    public static void main(String[] args) {
       // m1(10);
       // m1(10,20);
        m1(10,20,30);
        m2("jack");
        m2("jack","tom");
        m2("jack","tom","lisa");
    }

    public static void m1(int... a){
        System.out.println("m1方法执行了");
        for (int num : a){
            System.out.println(num);
        }
    }
    public static void m2(String... name){
        System.out.println("m2方法执行了");
    }

}

数组的扩容操作

  • 一维数组的扩容
    • 数组长度一旦确定不可变
    • 那数组应该如何扩容?
      • 只能创建一个更大的数组将原数组中的数据全部拷贝到新数组中
      • 可以使用System.arraycopy()方法完成数组的拷贝
    • 数组扩容会影响程序的执行效率,因此尽可能预测数据量,创建一个接近数量的数组,减少扩容次数
package com.powernode.javase;

/**
 * 关于数组的扩容问题:
 *      1. 数组的长度一旦确定不可变
 *      2. 如果数组满了,想存储更多的数据,只能创建一个新的数组,将原数组中的数据全部拷贝到新数当中,这样就可以完成扩容了
 *      3.怎么优化? 建议减少扩容次数. 建议预测数据量,创建一个容量差不多的数组.
 *      4.扩容的时候,需要使用到数组的拷贝,怎么拷贝?
 *          System.arraycopy()
 */
public class ArrayTest10 {
    public static void main(String[] args) {
        //原数组
        int[] src = {1,2,3,4,5,6,7,8,9,1};
        //新数组
        int[] dest = new int[src.length * 2];

        //拷贝
        System.arraycopy(src, 3, dest, 2, src.length - 3);

        for (int num : dest){
            System.out.println(num);
        }

    }
}

二维数组

  • 二维数组是一个特殊的一维数组,特殊在: 这个一维数组中每个元素是一个一维数组

二维数组的静态初始化

package com.powernode.javase;

/**
 * 1. 二维数组的静态初始化.
 * 2. 二维数组中元素的访问.
 */
public class ArrayTest11 {
    public static void main(String[] args) {

        //静态初始化二维数组第一种方式
        int[][] arr = new int[][]{
                {2, 2, 3, 3, 2, 1},
                {4, 5, 6, 2},
                {7, 8, 9}
        };
        System.out.println("该二维数组有" + arr.length + "个一维数组");
        System.out.println(arr[0].length);
        System.out.println(arr[1].length);
        System.out.println(arr[2].length);

        //找二维数组中第一个数组的第一个元素
       /* int[] arr0 = arr[0];
        int arr00 = arr0[0];*/
        int arr00 = arr[0][0];
        System.out.println("二维数组中第一个一维数组的第一个元素是 " + arr00);

        //二维数组中最后一个数组的最后一个元素
        System.out.println("二维数组中最后一个数组的最后一个元素是 "
                + arr[arr.length - 1][arr[arr.length - 1].length - 1]);

        //静态初始化二维数组第一种方式
        int[][] arr2 ={
                {2, 2, 3, 3, 2, 1},
                {4, 5, 6, 2},
                {7, 8, 9}
        };
    }
}

二维数组的动态初始化

package com.powernode.javase;

/**
 * 二维数组的动态遍历
 *      1. int[][] arr = new int[3][4]; //等长
 *      2. int[][] arr = new int[3][];  //不等长
 */
public class ArrayTest12 {
    public static void main(String[] args) {

        //动态初始化一个二维数组(等长)
        int[][] arr = new int[3][4];

        //遍历等长二维数组
        for (int i = 0; i < arr.length; i++){
            for (int j = 0; j < arr[i].length; j++){
                System.out.print(arr[i][j] + " ");
            }
            System.out.println();
        }

        //动态初始化一个二维数组(不等长)
        int[][] arr1 = new int[][]{
                {1,2,3,4},
                {1,1,1,1,1,1},
                {2,2,2}
        };

        //遍历不等长二维数组
        for (int i = 0; i < arr1.length; i++){
            for (int j = 0; j < arr1[i].length; j++){
                System.out.print(arr1[i][j] + " ");
            }
            System.out.println();
        }
    }
}

酒店管理系统

package com.powernode.javase.hotel;

/**
 * 酒店管理系统
 */
public class Room {

    //房间编号
    private int roomNo;
    //房间类型
    private String type;
    //房间状态
    private boolean state;

    public Room() {
    }

    public Room(int roomNo, String type, boolean state) {
        this.roomNo = roomNo;
        this.type = type;
        this.state = state;
    }

    public int getRoomNo() {
        return roomNo;
    }

    public void setRoomNo(int roomNo) {
        this.roomNo = roomNo;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public boolean isState() {
        return state;
    }

    public void setState(boolean state) {
        this.state = state;
    }

    @Override
    public String toString() {
        return "[" + this.roomNo + "," +this.type + ","+(this.state ? "占用" : "空闲")+"]";
    }
}

package com.powernode.javase.hotel;

/**
 * 酒店
 */
public class Hotel {
    //酒店中的房间对象
    private Room[][] rooms = new Room[6][10];


    /**
     * 通过构造方法new一个酒店出来
     */
    public Hotel() {
        for (int i = 0; i < rooms.length; i++) {
            for (int j = 0; j < rooms[i].length; j++) {
                if (i == 0 || i == 1) {
                    rooms[i][j] = new Room((i + 1) * 100 + 1 + j, "单人间", false);
                }else if(i == 2 || i == 3){
                    rooms[i][j] = new Room((i+1) * 100 + 1 + j, "双人间",false);
                }else if(i == 4 || i == 5){
                    rooms[i][j] = new Room((i + 1) * 100 + 1 + j,"豪华间",false);
                }
            }
        }
    }

    /**
     * 预订房间
     * @param roomNo 房间编号
     */
    public void order(int roomNo){
        //根据房间编号确定i和j的值
        //101 i=0 j=0
        rooms[roomNo / 100 - 1][roomNo % 100 - 1].setState(true);
    }

    /**
     * 退房
     * @param roomNo 房间编号
     */
    public void exit(int roomNo){
        rooms[roomNo / 100 - 1][roomNo % 100 - 1].setState(false);
    }

    public void display(){
        for (int i = 0; i < rooms.length; i++) {
            for (int j = 0; j < rooms[i].length; j++) {
                System.out.print(rooms[i][j]);
            }
            System.out.println();
        }
    }
}
package com.powernode.javase.hotel;

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        //创建hotel对象
        Hotel hotel = new Hotel();
        System.out.println("欢迎使用酒店管理系统,请认真阅读以下使用说明");
        System.out.println("请通过选择对应的功能编号来使用相应的功能: ");
        System.out.println("[1]查看酒店所有的房间状态");
        System.out.println("[2]预订房间");
        System.out.println("[3]退房");
        System.out.println("[0]退出系统");
        Scanner s = new Scanner(System.in);

        while(true) {
            //获取功能编号
            System.out.print("请输入要执行的功能编号: ");
            int no = s.nextInt();
            switch (no) {
                case 1:
                    hotel.display();
                    break;
                case 2:
                    System.out.print("请输入预定的房间号: ");
                    int roomNo1 = s.nextInt();
                    hotel.order(roomNo1);
                    System.out.println("预订成功");
                    break;
                case 3:
                    System.out.print("请输入退房的房间号: ");
                    int roomNo2 = s.nextInt();
                    hotel.exit(roomNo2);
                    System.out.println("退房成功");
                    break;
                case 4:
                    System.out.println("再见,欢迎下次使用!");
                    return;
            }
        }
    }
}

学生管理系统

数据结构与算法

数据结构概述

  • 数据结构是指用来存储和组织数据的一种方式,就像在生活中我们用文件柜,书架,衣柜等来整理我们的物品一样,数据结构也可以帮助我们整理和管理程序中的数据
  • 数据结构分为:数据的逻辑结构.数据的物理结构
    • 逻辑结构是指数据元素之间的逻辑关系,他是从抽象的角度描述数据元素之间的关系,不涉及具体的存储方式或实现细节.逻辑结构主要关注问题的本质,特点和抽象模型,是数据结构的逻辑表示
    • 物理结构是指数据结构在计算机内存中实际存储和组织的方式.他是从具体的角度描述数据结构的实现方式和存储结构,包括数据元素在内存中的存储分布和访问方式等.物理结构主要关注问题的具体实现和操作.
    • 因此,逻辑结构与物理结构的区别在于:逻辑结构是从抽象的角度描述数据元素之间的关系,物理结构是从具体的角度描述内存中数据元素的存储方式和组织形式.逻辑结构主要关注问题的本质和特点,物理结构主要关注问题的具体实现和操作.
  • 逻辑结构的划分?
    • 集合结构:数据结构中的元素之间除了在"同属一个集合"的关系外,别无其他关系;
    • 线性结构:数据结构中的元素存在"一对一"的线性关系,例如:冰糖葫芦.
    • 树形结构:数据结构中的元素存在"一对多"的层次关系,例如公司组织架构
    • 图形结构或者网状结构:数据结构中的元素存在"多对多的"任意关系,例如:地图
  • 物理结构如何划分?
    • 顺序存储结构:用一组连续的存储空间单元来依次的存储数据元素,例如数组.
    • 链式存储结构:用一组任意的存储单元来存储元素,通过保存地址找到相关联的元素,元素之间的逻辑关系用引用来表示,例如:链表
    • 散列存储结构:根据节点key计算出该节点的存储地址.例如:java集合中的HashMap采用了散列式存储结构,添加,查询速度都很快

算法概述

  • 什么是算法
    • 算法就是解决问题的方法和步骤,可以让计算机完成特定任务,并提高计算机系统的效率和性能.一个良好的算法可以提高程序的执行率
    • 时间复杂度:评估执行程序所需的时间,可以估算出程序对处理器的使用程度
    • 空间复杂度:评估执行程序所需的存储空间,可以估算出程序对计算机内存的使用程度

数据结构与算法的关系

  • 程序的灵魂= 数据结构 + 算法
  • 数据结构可以提供算法的运行环境和基础,而算法又可以通过对数据结构的设计和操作,实现对数据的高效管理和处理.数据结构和算法是相互依存的,应该统一考虑,合理利用不同的数据结构和算法来解决实际问题,从而提高程序的执行效率
  • 高效的程序=恰当的数据结构+合适的算法

常见的时间复杂度耗时比较

  • 算法的时间复杂度是一个算法好坏的重要指标.一般情况下,随着规模n的增大,T(n)的增长较慢的算法为最优算法
  • 常用的时间复杂度按照耗费的时间从小到大依次是:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2^n)<O(n!)

数组的排序算法

冒泡排序
package com.powernode.javase;

import java.util.Arrays;

/**
 * 冒泡排序
 */
public class ArrayTest13 {
    public static void main(String[] args) {
        int[] arr = {1,3,4,2,5};

        bubbleSort(arr);
        System.out.println(Arrays.toString(arr));

    }
    /*private static void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length-1; i++){
            for (int j = 0; j < arr.length-1-i; j++){
                if (arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[i] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }*/

    /**
     * 对bubbleSort方法进行优化
     */
    private static void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length-1; i++){
            //假设顺序为从小到大,不需要排序
            boolean flag = true;
            for (int j = 0; j < arr.length-1-i; j++){
                if (arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[i] = arr[j+1];
                    arr[j+1] = temp;
                    flag = false;
                }
            }
            if(flag){
                break;
            }
        }
    }

}

选择排序
package com.powernode.javase;

import java.util.Arrays;

/**
 * 选择排序:
 *      核心原理:找出参与比较的这些数据中最小的,然后拿着这个最小的数据和参与比较的这堆数据中最左边的元素交换位置
 *      优点:交换的次数比冒泡排序的次数少.执行效率比冒泡排序效率高.(冒泡排序中的元素每一次比较完之后就交换,有些交换是没有意义的)
 */
public class ArrayTest14 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,6,4};
        selectSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    private static void selectSort(int[] arr) {
        for (int i = 0; i < arr.length-1; i++){
            //假定最左边的数据是最小的
            int min = i;
            for (int j = i+1; j < arr.length; j++){
                if (arr[j] < arr[min]){
                    min = j;
                }
            }
            if (min != i){
                int temp = arr[min];
                arr[min] = arr[i];
                arr[i] = temp;
            }
        }
    }
}

线性查找
package com.powernode.javase;


/**
 * 线性查找算法,一个一个遍历,一个一个找,不需要排序
 */
public class ArrayTest15 {
    public static void main(String[] args) {
        int[] arr = {12,41,24,35,453,35,45,65,76};
        int num = 450;
        int index = search(arr,num);
        System.out.println(index >= 0 ? index : "输入的数据不存在");
    }

    private static int search(int[] arr, int num) {
        for (int i = 0; i < arr.length; i++){
            if (arr[i] == num) {
                return i;
            }
        }
        return -1;
    }
    }

二分法查找
package com.powernode.javase;

/**
 * 二分法查找/折半查找算法
 * 1.
 */
public class ArrayTest16 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6,7,8,9,101};
        int num = 1;
        int index = binarySearch(arr,num);
        System.out.println(index >= 0 ? num + "的下标是: " + index : "找不到数据");
    }

    private static int binarySearch(int[] arr, int num) {
        int begin = 0;
        int end = arr.length -1;
        while(begin <= end){
            int mid = (begin + end) / 2;
            if (arr[mid] == num){
                return mid;
            }else if(arr[mid] < num){
                begin = mid + 1;
            }else if(arr[mid] > num){
                end = mid - 1;
            }
        }
        return -1;
    }
}


Arrays工具类

  • Arrays.toString()方法:将数组转换成字符串
  • Arrays.deepToString()方法:可以将二维数组转换成字符串
  • Arrays.equals(int[] arr1, int[] arr2)方法:判断两个数组是否相等
  • Arrays.equals(Object[] arr1, Object[] arr2)方法:
  • Arrays.deepEquals(Object[] arr1, Object[] arr2)方法:判断两个二维数组是否相等
  • Ararys.sort(int[] arr)方法:基于快速排序算法,适合小型数据量排序
  • Arrays.sort(String[] arr)方法:
  • Arrays.paralleSort(int[] arr)方法:基于分治的归并排序算法,支持多核cpu排序,适合大数据量排序
  • Arrays.binarySearch(int[] arr, int num)方法:二分法查找
  • Arrays.fill(arr,val)方法:填充
  • Arrays.copyOf(arr,length)方法:数组拷贝
  • Arrays.copyOfRange(arr,from,to)方法:
  • Arrays.asList(index)方法:将一组数据转换成List集合
package com.powernode.javase;

import org.junit.Test;

import java.util.Arrays;

/**
 *  测试数组的工具类java.util.Arrays
 *  测试用例.
 *  Arrays.toString(arr)方法的作用是: 将数组对象转换成字符串形式
 */

public class ArrayTest18 {
    /**
    Arrays.toString(arr)方法的作用是: 将数组对象转换成字符串形式
    */
     @Test
    public void testToString(){
        int[] arr = {1,2,3,4,5};
        System.out.println(arr);
        System.out.println(Arrays.toString(arr));

        String[] names = {"张三","李四","王五"};
         System.out.println(names);
         System.out.println(Arrays.toString(names));
    }

    /**
     * 适合于二维数组以及多维数组
     */
    @Test
    public void testDeepToString(){
         int[][] arr = {
                 {1,2,3,4,5},
                 {111,11,1,1,1,1},
                 {2,3,4,1,14,2}
         };
        System.out.println(arr);
        System.out.println(Arrays.deepToString(arr));
    }
}

package com.powernode.javase;

import java.util.Arrays;

public class ArrayTest19 {
    public static void main(String[] args) {
        int[] arr1 = {1, 2, 3};
        int[] arr2 = {1, 2, 3};
        System.out.println(Arrays.equals(arr1, arr2));

        String[] name1 = {"张三","李四","王五"};
        String[] name2 = {"张三","李四","王五"};
        System.out.println(Arrays.equals(name1,name2));

        int[][] arr3 = {
                {1,2,3},
                {1,1,1},
                {1,2,3}
        };
        int[][] arr4 ={
                {1,2,3},
                {1,1,1},
                {1,2,3}
        };
        System.out.println(Arrays.deepEquals(arr3,arr4));
    }
}
package com.powernode.javase;

import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Random;

public class ArrayTest20 {
    @Test
    //将一串数据转换成list集合
    public void testAsList(){
            List list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
            for (int i = 0; i < list.size(); i++){
                System.out.println(list.get(i));
            }
    }


    @Test
    public void testCopyOf(){
        int[] arr = {1,2,3,4,5,6,7};
        int[] newArr = Arrays.copyOf(arr,4);
        System.out.println(Arrays.toString(newArr));//[1, 2, 3, 4]

        int[] newArr2 = Arrays.copyOfRange(arr,2,4);
        System.out.println(Arrays.toString(newArr2));//[3, 4]
    }


    @Test
    public void testFill(){
        int[] arr = new int[5];
        Arrays.fill(arr,10);
        System.out.println(Arrays.toString(arr));//[10, 10, 10, 10, 10]

        Arrays.fill(arr,1,3,100);
        System.out.println(Arrays.toString(arr));//[10, 100, 100, 10, 10] 不包含toIndex所在的下标
    }

    //二分法查找
    @Test
    public void binarySearch(){
        int[] arr = {1,2,3,4,5,6,7,8};
        int num = 7;
        System.out.println(Arrays.binarySearch(arr,num));
    }


    @Test
    public void testSort(){
        int[] arr = {2,4,1,5};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));

        //按照字典顺序排列的
        String[] names = {"a","ac","b","ab"};
        Arrays.sort(names);
        System.out.println(Arrays.toString(names));

        //能不能对Person数组排序
        /*
        凡是自定义的类型要作比较的话,这个自定义类型必须实现一个接口:Comparable接口,并且实现compareTo方法,在这个方法中编写比较规则
         */
        Person p1 = new Person(21);
        Person p2 = new Person(24);
        Person p3 = new Person(19);
        Person p4 = new Person(29);

        Person[] p = {p1,p2,p3,p4};
        Arrays.sort(p);
        System.out.println(Arrays.toString(p));
    }

    /**
     * 启用多核CPU并行排序
     * 首先你的电脑是支持多核的,
     * 注意:数据量太小的话,不要调用这个方法,因为启动多核也是需要耗费资源的
     * java8引入的方法
     * 数据量较大的时候,建议使用这个方法效率比较高
     *
     * 通过源码分析:如果超过4096个长度,则会启用多核
     */
    @Test
    public void testParallelSort(){
        int[] arr = new int[100000000];
        Random random = new Random();
        for (int i = 0; i < 100000000; i++){
            arr[i] = random.nextInt(100000000);
        }
        //获取系统当前的毫秒数
        long begin = System.currentTimeMillis();
        //Arrays.parallelSort(arr);//942
        Arrays.sort(arr);//7083
        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }

}

第五章 异常

异常的概述

package com.powernode.javase;

/**
 * 1.什么是异常? 异常机制在java中有什么用?
 *      什么是异常:java程序执行过程中的意外,错误等出现的不正常的情况
 *      异常机制在java中的作用: 提高程序的健壮性.
 *
 * 2.在java语言中,异常是以什么形式存在的?
 *      以类和对象的形式存在的
 *      定义异常其实本质上就是定义一个类.
 *      异常如果发生的话,也需要通过这个异常类new对象
 */
public class ExceptionTest01 {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        if (b == 0) {
            System.out.println("除数不能为0");
            return;
        }
        int c = a / b;
        System.out.println(c);

    }

    /*Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.powernode.javase.ExceptionTest01.main(ExceptionTest01.java:17)*/

}

异常继承结构

  • 所有的异常和错误都是可抛出的.都继承了Throwable类

  • Error是无法处理的,出现后只有一个结果:JVM终止

  • Exception是可以处理的

  • Exception的分类:

    • 所有的RuntimeException的子类: 运行时异常/未检查异常(UncheckedException)/非受控异常
    • Exception的子类(除了RuntimeException之外): 编译时异常/检查异常(CheckedException)/受控异常
  • 编译时异常和运行时异常的区别:

    • 编译时异常的特点:在编译阶段必须提前处理,如果不处理的话编译器报错
    • 运行时异常的特点:在编译阶段可以选择处理,也可以不处理,没有硬性要求
    • 编译时异常一般是由外部环境或外在条件引起的,如网络故障,磁盘空间不足,文件找不到等
    • 运行时异常一般是由程序员的错误引起的,并且不需要强制进行异常处理
    • 注意:编译时异常并不是在编译阶段发生的异常,所有的异常发生都是在运行阶段的,因为每个异常发生都是会new异常对象的,new异常对象只能在编译阶段完成.那为什么叫做编译时异常呢?这是因为这种异常必须在编译阶段提前预处理,如果不处理编译器报错,因此而得名编译时异常
  • 异常是如何发生的

package com.powernode.javase;

/**
 * 异常在程序中到底是如何发生的?
 */
public class ExceptionTest02 {
    public static void main(String[] args) {

        //异常的发生需要两个阶段
        //第一个阶段:创建异常对象
        //NullPointerException e = new NullPointerException();
        //第二个阶段:让异常发生(手动抛出异常)
        //throw e;

        //合并一步
        throw new NullPointerException();
    }
}

自定义异常

  • 第一种解决异常的方式: throws
package com.powernode.javase.exception;

/**
 * 无效名字异常,当名字长度小于6位,或者大于12位,认为程序出现了异常
 *
 * 自定义异常分为两步:
 *      第一步:编写类继承RuntimeException或者Exception
 *      第二步:提供两个构造方法,一个无参数的,一个带有String参数的,并且在构造方法中调用super(String)
 */
public class IllegalNameException extends Exception{
    public IllegalNameException() {
    }

    public IllegalNameException(String message) {
        super(message);
    }
}

package com.powernode.javase.exception;

/**
 * 无效年龄异常,凡是年龄小于18岁的都是异常.
 */
public class IllegalAgeException extends Exception{
    public IllegalAgeException() {
    }

    public IllegalAgeException(String message) {
        super(message);
    }
}

package com.powernode.javase.exception;

import java.util.Scanner;

public class UserService {
    public static void main(String[] args) throws IllegalNameException, IllegalAgeException {
        System.out.println("欢迎使用本系统");
        Scanner s = new Scanner(System.in);
        System.out.print("请输入用户名: ");
        String name = s.next();
        System.out.print("请输入年龄: ");
        int age = s.nextInt();
        register(name, age);
    }
    public static void register(String name, int age) throws IllegalAgeException, IllegalNameException{
        if (name.length() < 6 || name.length() > 12){
            throw new IllegalNameException();
        }
        if (age < 18){
            throw new IllegalAgeException();
        }
        System.out.println("注册成功!");
    }
}

  • 第二种解决异常的方式: try catch
package com.powernode.javase.exception;

import java.util.Scanner;

/**
 * 异常处理的第二种方式: 捕捉
 *  语法格式:
 *      try {
 *          //需要尝试执行的程序,这里的程序有可能会出现异常.
 *          ..
 *          ..如果执行到这里出现了异常,则try块的后续代码不会执行
 *          ..
 *          ..
 *      }catch (异常类型1 变量名) {
 *          // 当捕捉到的异常是"异常类型1"时, 走这个分支, 进行异常的处理
 *      }catch (异常类型2 变量名) {
 *          // 当捕捉到的异常是"异常类型2"时, 走这个分支, 进行异常的处理
 *      }catch (异常类型3 变量名) {
 *          // 当捕捉到的异常是"异常类型3"时, 走这个分支, 进行异常的处理
 *      }...
 *      
 *      // 后续Java代码可以执行
 *      
 *      另外注意:
 *          catch语句块可以看做是分支 try catch 语句中,最多只有一个catch分支执行
 *          catch 可以写多个,但是必须要遵循自上而下,从小到大
 */
public class UserService {
    public static void main(String[] args){
        System.out.println("欢迎使用本系统");
        Scanner s = new Scanner(System.in);
        System.out.print("请输入用户名: ");
        String name = s.next();
        System.out.print("请输入年龄: ");
        int age = s.nextInt();
        try {
            register(name, age);
        }catch (IllegalNameException e){
            System.out.println("用户名输入错误");
        }catch (IllegalAgeException a){
            System.out.println("年龄输入错误");
        }
    }
    public static void register(String name, int age) throws IllegalAgeException, IllegalNameException{
        if (name.length() < 6 || name.length() > 12){
            throw new IllegalNameException();
        }
        if (age < 18){
            throw new IllegalAgeException();
        }
        System.out.println("注册成功!");
    }
}

异常的常用方法

  • 获取异常的简单描述信息:
    • exception.getMessage();
    • 获取message是通过构造方法创建异常对象时传递过去的message
  • 打印异常堆栈信息:
    • exception.printStackTrace();
  • 要会看异常的堆栈信息:
    • 异常信息的打印是符合栈数据结构的
    • 看异常信息主要看最开始的信息描述,看栈顶信息

finally语句块

  • finally语句块的代码是一定会执行的

  • finally语句块不能单独使用,至少需要配合try语句块一起使用:

    • try{}finally{}
    • try{}catch{}finally{}
  • final,finally,finalize分别是什么?

    • final是一个关键字,修饰的类无法继承,修饰的方法无法覆盖,修饰的变量不能修改
    • finally是一个关键字,和try一起使用,finally语句块中的代码一定会执行
    • finalize是一个标识符,他是Object类中的一个方法名

方法的覆盖与异常

子类继承父类之后,重写了父类的方法,不能抛出更多的异常,可以抛出更少的异常

第六章 常用类

String类

String类的字符串拼接

  • Java中的String属于引用数据类型,代表字符串.
  • Java专门在堆中为字符串准备了一个字符串常量池.因为字符串使用比较频繁,放在字符串常量池中省去了对象的创建过程,从而提高了程序的执行效率.(常量池属于一种缓存技术,缓存技术是提高程序执行效率的重要手段)
    • String s1 = “hello”;
    • String s2 = “hello”;
    • System.out.println(s1 == s2); // true 说明s1和s2指向了字符串常量池中的同一个字符串对象
    • 注意: 字符串字面量在编译的时候就已经确定将其放到字符串常量池中,JVM启动时会立即将程序中带有双引号的字符串全部放入字符串常量池.
  • Java8之后字符串常量池在堆中,Java8之前字符串常量池在永久代
  • 字符串一旦创建是不可变的. (底层String源码中有一个属性: private final byte[] value:)
    • String s = “hello” ; 其中"hello" 存储在字符串常量池中
    • “hello"不可变,不能变成"hello123”, 如果进行字符串拼接,必然创建新的字符串对象.
    • 是"hello"不可变,不是s不可变,s可以指向其他的字符串对象: s = “xyz”;
package com.powernode.javase;

/**
 * 使用 + 进行拼接生成的新的字符串不会被放到字符串常量池当中
 *
 * 当 + 两边都是字符串字面量的时候,编译器会进行自动优化,在编译阶段进行拼接
 */
public class StringTest01 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "world";
        String s3 = s1 + s2;
        String s4 = "helloworld";

        // s3 指向的对象,没有在字符串常量池中,在堆中
        //底层实际上在进行 + 的时候(这个 + 两边至少有一个是变量),会创建一个StringBuilder对象,进行字符串拼接
        //最后的时候会自动调用StringBuilder对象的toString()方法,再将StringBuilder转换成String对象
        System.out.println(s3 == s4);//false

        //此时编译器进行了优化,在编译阶段就进行了拼接
        //所以s4和s5指向同一个对象
        //只有在+两边都是""的情况下才可以
        String s5 = "hello" + "world";
        System.out.println(s4 == s5);//true
    }
}

String类的构造方法

package com.powernode.javase;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class StringTest02 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        char[] chars = {'我','是','中','国','人'};
        String s = new String(chars);
        System.out.println(s);

        String s2 = new String(chars,2,3);
        System.out.println(s2);

        byte[] bytes = {97,98,99,100};
        String s3 = new String(bytes);
        System.out.println(s3);

        String s4 = new String(bytes,0,2);
        System.out.println(s4);

        //乱码的本质,在进行编码和解码的时候没有使用同一个字符编码方式
        byte[] bs = "库库学Java".getBytes(StandardCharsets.UTF_8);
        String s5 = new String(bs,StandardCharsets.UTF_8);
        System.out.println(s5);

        //在不知道字符编码方式的时候,可以动态获取平台的编码方式.(使用平台默认的字符集进行编码)
        byte[] bs2 = "库库学Java".getBytes(Charset.defaultCharset());

        String s6 = new String(bs2,Charset.defaultCharset());

        System.out.println(s6);

    }
}

String的常用方法

  • char charAt(int index) : 返回索引处的char值
  • int length(): 获取字符串长度
  • boolean isEmpty(): 判断字符串是否为空字符串, 如果length()是0就是空字符串
  • boolean equals(Object object): 判断两个字符串是否相等
  • boolean equalsIgnoreCase(String otherString): 判断两个字符串是否相等,忽略大小写
  • boolean contains(CharSequence s): 判断当前字符串是否包含某个子字符串
  • boolean startsWith(String prefix): 判断当前字符串是否以某个字符串开头
  • boolean endsWith(String suffix): 判断当前字符串是否以某个字符串结尾
  • int compareTo(String anthorString): 两个字符串按字典顺序比较大小
  • int compareToIgnoreCase(String anthorString): 两个字符串按字典顺序比较大小,比较时忽略大小写
  • int indexOf(String str): 获取当前字符串中str字符串的第一次出现时的下标
  • int indexOf(String str, int fromIndex): 从当前字符串的fromIndex下标开始往右搜索,获取当前字符串中str字符第一次出现时的下标
  • int lastIndexOf(String str):获取当前字符串中str字符最后一次出现处的下标
  • int lastIndexOf(String str, int fromIndex): 从当前字符串的fromIndex下标开始往左搜索,获取当前字符串中最后一次出现str字符的下标
  • byte[] getBytes(): 将字符串转换成字节数组,其实就是对字符串进行编码
  • byte[] getBytes(String charsetName): 将字符串按照指定字符集的方式进行编码
  • byte[] getBytes(Charset charset):
  • char[] toCharArray(): 将字符串转换成字符数组
  • String toLowerCase(): 转小写
  • String toUpperCase(): 转大写
  • String concat(String str): 进行字符串的拼接操作. 和 + 的 区别?
    • +既可以进行求和,也可以进行字符串的拼接,底层拼接的时候会创建StringBuilder对象进行拼接. + 拼接null时不会出现空指针异常
    • concat方法参数只能是字符串类型,拼接时不会创建StringBuilder对象,拼接完成后返回一个新的String对象.拼接null会出现空指针异常
    • +使用较多. 如果进行大量字符串拼接,这两个都不合适
  • String substring(int beginIndex): 从指定下标beginIndex开始截取子字符串
  • String substring(int beginIndex, int endIndex):
  • String trim(): 去除字符串前后空白(只能去除ASCII码中的空格和制表符)
  • String strip(): 去除字符串前后空白(支持所有的编码形式的空白,可以将全角空白去除,\u300是全角空格)
  • String stripLeading(): 去除前空白
  • String stripTrailing(): 去除后空白
  • String toString():
  • String intern(): 获取字符串常量池中的字符串,如果常量池中没有,则字符串加入常量池并返回.
    • byte[] bytes = {97,98,99,100}; String s = new String(bytes);
    • String s2 = s.intern(); //将字符串"abcd"放入字符串常量池并返回常量池中的字符串"abcd"
  • static String join(CharSequence d, CharSequence… elements): 将多个字符串以某个分隔符连接
  • static String valueOf(int i): 将非字符串类型的数据转换为字符串形式

正则表达式

看第六章ppt第11页

String常见面试题

看第六章ppt第15页

StringBuffer和 StringBilder

  • 这两个类是专门为了频繁的字符串拼接准备的
  • StringBuffer先出现的,Java5的时候新增了StringBuilder.StringBuffer是线程安全的,在不考虑线程安全的情况下使用StringBuilder,效率更高一点.
  • 底层是一个byte[]数组,并且这个byte[]数组没有被final修饰,说明如果byte[]数组满了,可以通过创建一个更大的数组来达到扩容,然后他可以重新指向这个新的数组对象
  • 优化策略: 创建StringBuilder对象的时候,预估字符串的长度,给定一个合适的初始化容量,减少底层数组的扩容
  • StringBuilder默认初始化容量:16
  • 扩容策略是:从当前扩容开始,每次扩容为原来的2倍,再加2
两者的构造方法
  • StirngBuilder() 构造一个字符串生成器,其中不包含任何字符,初始容量为16;
  • StringBuilder(int capacity) 构造一个字符串生成器,其中不包含任何字符,并且具有容量参数指定的初始容量
  • StringBuilder(String str) 构造初始化为指定字符串内容的字符串生成器,
StringBuilder 的常用方法
  • append (type data) : 将指定类型的数据追加到StringBuilder对象的末尾,实现字符串的拼接
  • delete(int start, int end) : 删除从start索引(包括)开始到 end索引(不包括)之间的字符
  • deleteCharAt(int index) : 删除索引指定位置的字符
  • insert(int offset, String str) : 在指定的偏移量sffset位置插入字符串str
  • replace(int start, int end ,String str) : 用字符串str替换从start索引(包括)到end索引(不包括)之间的字符
  • reverse() : 将StringBuilder的字符顺序反转
  • setCharAt(int index, char ch): 将指定索引index位置的字符设置为ch
  • setLength(int newLength): 设置StringBuilder的长度, 如果newLength小于当前长度,会截断后面的字符;如果大于当前长度,会在后面填充\0字符

包装类

  • 什么是包装类?有什么用?

    • 为了方便开发,Java为8种基本数据类型分别又提供了对应的包装类(包装类都是引用数据类型)

      八种基本数据类型 包装类

      ================================================================

      byte java.lang.Byte

      short java.lang.Short

      int java.lang.Integer

      long java.lang.Long

      float java.lang.Float

      double java.lang.Double

      boolean java.lang.Boolean

      char java.lang.Character

包装类中的前六个都继承了Number类,因此类中都具有以下方法:

  • byteValue()
  • shortValue()
  • intValue()
  • longValue()
  • floatValue()
  • doubleValue()

这些方法的作用就是将包装类型的数据转换成基本数据类型,这个过程叫做"拆箱"

boolean拆箱的方法是: booleanValue()

char拆箱的方法是: charValue()

Integer的常量

通过Integer提供的常量可以获取int的最大值和最小值

  • 最大值:Integer.MAX_VALUE
  • 最小值:Integer.MIN_VALUE

当然其他类型如byte,char…也都含有最值常量

Integer的构造方法
  • Integer(int value): 可以将int类型转换成包装类型
  • Integer(String str): 必须是数字类型字符串, 不然会出现NumberFormatException(数字匹配异常)

其他包装类也有构造方法,但在Java9之后都已经标记过时,不建议使用

Integer的常用方法
  1. static int compare(int x, int y)
    • 作用:比较两个 int 类型数值 xy 的大小。若 x < y,返回负整数;若 x == y,返回 0;若 x > y,返回正整数。
    • 示例int result = Integer.compare(3, 5);result 的值为 -2
  2. static int max(int a, int b)
    • 作用:返回两个 int 类型数值 ab 中的较大值。
    • 示例int maxValue = Integer.max(3, 5);maxValue 的值为 5
  3. static int min(int a, int b)
    • 作用:返回两个 int 类型数值 ab 中的较小值。
    • 示例int minValue = Integer.min(3, 5);minValue 的值为 3
  4. static int parseInt(String s)
    • 作用:将包含数字的字符串 s 解析成 int 类型的数值。如果字符串格式不正确(例如包含非数字字符),会抛出 NumberFormatException 异常。
    • 示例int num = Integer.parseInt("123");num 的值为 123
  5. static String toBinaryString(int i)
    • 作用:将 int 类型的数值 i 转换为其二进制表示形式的字符串。
    • 示例String binaryStr = Integer.toBinaryString(5);binaryStr 的值为 "101"
  6. static String toHexString(int i)
    • 作用:将 int 类型的数值 i 转换为其十六进制表示形式的字符串。
    • 示例String hexStr = Integer.toHexString(10);hexStr 的值为 "a"
  7. static String toOctalString(int i)
    • 作用:将 int 类型的数值 i 转换为其八进制表示形式的字符串。
    • 示例String octalStr = Integer.toOctalString(8);octalStr 的值为 "10"
  8. int compareTo(Integer anotherInteger)
    • 作用:比较当前 Integer 对象与 anotherInteger 的大小。若当前对象小于 anotherInteger,返回负整数;若相等,返回 0;若大于,返回正整数。因为 Integer 实现了 Comparable 接口。
    • 示例Integer num1 = 3; Integer num2 = 5; int result = num1.compareTo(num2);result 的值为 -1
  9. boolean equals(Object obj)
    • 作用:比较当前 Integer 对象与另一个对象 obj 是否相等。Integer 重写了 equals 方法,当 obj 也是 Integer 类型且数值相等时返回 true,否则返回 false
    • 示例Integer num1 = 3; Integer num2 = 3; boolean isEqual = num1.equals(num2);isEqual 的值为 true
  10. String toString()
    • 作用:将 Integer 对象转换为字符串形式。Integer 重写了该方法,返回表示其数值的字符串。
    • 示例Integer num = 5; String str = num.toString();str 的值为 "5"
  11. int intValue()
    • 作用:将 Integer 包装类对象拆箱为基本的 int 数据类型。
    • 示例Integer num = 5; int basicNum = num.intValue();basicNum 的值为 5
  12. static String toString(int i)
    • 作用:将基本的 int 类型数值 i 转换为字符串形式。
    • 示例String str = Integer.toString(5);str 的值为 "5"
  13. static Integer valueOf(int i)
    • 作用:将基本的 int 类型数值 i 转换为 Integer 包装类对象。
    • 示例Integer num = Integer.valueOf(5);num 是一个值为 5Integer 对象。
  14. static Integer valueOf(String s)
    • 作用:将包含数字的字符串 s 转换为 Integer 包装类对象。如果字符串不是有效的数字字符串,会抛出 NumberFormatException 异常。
    • 示例Integer num = Integer.valueOf("5");num 是一个值为 5Integer 对象。
Integer,String,int三者之间的相互转换
package com.powernode.javase.StringBuilder;

/**
 * Integer,int,String之间的相互转换
 */
public class IntegerTest02 {
    public static void main(String[] args) {
        //int 转换成Integer
        int i1 = 100;
        Integer integer1 = Integer.valueOf(i1);
        System.out.println(integer1);

        //Integer 转换成int
        Integer integer2 = 100;
        int i2 = integer2.intValue();
        System.out.println(i2);

        //int 转换成String
        //第一种方式
        int i3 = 100;
        String s1 = i3 + "";
        System.out.println(s1);
        //第二种方式
        String s2 = String.valueOf(i3);
        System.out.println(s2);

        //String 转换成int
        String s3 = "123";
        int i4 = Integer.parseInt(s3);
        System.out.println(s3);

        //String 转换成Integer
        String s4 = "123";
        Integer integer3 = Integer.valueOf(s4);
        System.out.println(integer3);

        //Integer 转换成String
        Integer integer4 = 100;
        String s5 = String.valueOf(integer4);
        System.out.println(s5);

    }
}

自动装箱和自动拆箱
  • java5之后为了开发方便,引入了新特性自动装箱和自动拆箱

  • 自动装箱: auto boxing

    • Integer i = 10000; //实际是Integer i = new Integer(10000);
  • 自动装箱: auto unboxing

    • int num = i;//实际是int num = i.intValue();
  • System.out.println(i + 1); //这里的i 也会被自动拆箱,结果是10001

  • Integer a = null;

    int b = a;

    这里会出现空指针异常,实际是因为调用了a.intValue();

    因为a 是null,访问实例变量是会出现空指针异常,所以使用时应该注意

整数型常量池

[-128,127] Java为这个区间的Integer对象创建了整数型常量池

也就是说如果整数没有超过范围的话,直接从整数型常量池里获取对象

Integer i1 = 127;

Integer i2 = 127;

System.out.println(i1 == i2); //true

Integer i1 = 128;

Integer i2 = 128;

System.out.println(i1 == i2); //false

大数字

如果整数超过了long的最大值怎么办?

Java中提供了一种引用数据类型来解决这个问题, java.math.BigInteger, 他的父类是Number.

常用的构造方法 BigInteger(String val)

  • 常用的方法(实例方法):

    BigInteger add(BigInteger val); 求和
    BigInteger subtract(BigInteger val); 相减
    BigInteger multiply(BigInteger val); 乘积
    BigInteger divide(BigInteger val); 商
    int compareTo(BigInteger val); 比较
    BigInteger abs(); 绝对值
    BigInteger max(BigInteger val); 最大值
    BigInteger min(BigInteger val); 最小值
    BigInteger pow(int exponent); 次幂
    BigInteger sqrt(); 平方根

如果浮点型数据超过double的最大值怎么办?

java中提供了一种引用数据类型来解决这个问题:java.math.BigDecimal(经常用在财务软件中)。它的父类是Number。
构造方法:BigDecimal(String val)
常用方法:
BigDecimal add(BigDecimal augend); 求和
BigDecimal subtract(BigDecimal subtrahend); 相减
BigDecimal multiply(BigDecimal multiplicand); 乘积
BigDecimal divide(BigDecimal divisor); 商
BigDecimal max(BigDecimal val); 最大值
BigDecimal min(BigDecimal val); 最小值
BigDecimal movePointLeft(int n); 向左移动小数点
BigDecimal movePointRight(int n); 向右移动小数点

数字格式化

有时我们需要将数字以某种格式展示,在java中如何格式化呢?
java.text.DecimalFormat 类是专门用来对数字进行格式的。

常用数字格式:
###,###.## // 三个数字为一组,组和组之间使用逗号隔开,保留两位小数
###,###.0000 // 三个数字为一组,组和组之间使用逗号隔开,保留4位小数,不够补0

使用方式:

先创建一个df对象 DecimalFormat df = new DecimalFormat(格式: 例如 ###,###.##);

调用df 的format()方法 df.format(输入数字: 123456789.123);

日期处理

获取当前时间及格式化日期
package com.powernode.javase.Date;


import java.util.Date;

/**
 * java.util.Data  日期API
 */
public class DateTest01 {
    public static void main(String[] args) {
        //Data的构造方法
        //Data()
        //Data(long l)

        //获取系统当前时间
        Date date = new Date();
        System.out.println(date);//Wed Mar 12 17:49:33 CST 2025

        //获取指定时间
        Date date1 = new Date(1000);
        System.out.println(date1);//Thu Jan 01 08:00:01 CST 1970

        //获取当前时间的十分钟之前
        Date date2 = new Date(System.currentTimeMillis() - 1000 * 60 * 10);
        System.out.println(date2);//Wed Mar 12 17:39:33 CST 2025

    }
}

package com.powernode.javase.Date;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 日期的格式化
 *   SimpleDateFormat  format(Date date)  完成格式化日期对象
 *   SimpleDateFormat  parse(String str)  完成格式化日期对象的解析
 */
public class DateTest02 {
    public static void main(String[] args) throws ParseException {
        //获取当前时间
        //Java.util.Date 转换为 Java.lang.String
        Date date = new Date();

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");//年月日 时分秒 毫秒
        String str = sdf.format(date);
        System.out.println(str);   //2025-03-12 15:56:44 476

        //Java.lang.String 转换为Java.util.Date
        Date date2 = sdf.parse(str);
        System.out.println(date2);   //Wed Mar 12 15:56:44 CST 2025

    }
}

日期相关API
  1. 获取当前时间的日历对象:使用Calendar.getInstance()方法可以获取表示当前时间的Calendar对象,示例代码如下:
Calendar c = Calendar.getInstance(); 
  1. 获取日历中的某部分
  • int year = c.get(Calendar.YEAR);:获取年份。
    • Calendar.MONTH:获取月份,需要注意的是,0 表示 1 月,1 表示 2 月,以此类推,11 表示 12 月。
    • Calendar.DAY_OF_MONTH:获取本月的第几天。
    • Calendar.DAY_OF_YEAR:获取本年的第几天。
    • Calendar.HOUR_OF_DAY:小时,采用 24 小时制。
    • Calendar.HOUR:小时,采用 12 小时制。
    • Calendar.MINUTE:获取分钟。
    • Calendar.SECOND:获取秒。
    • Calendar.MILLISECOND:获取毫秒。
    • Calendar.DAY_OF_WEEK:获取星期几,1 表示星期日,2 表示星期一,……,7 表示星期六。
  1. 日历的方法(实例方法)

    set 方法:设置日历
    • 可以通过指定日历字段和对应的值来设置日历中的特定部分。

      • 示例一:仅设置年份为 2023
      calendar.set(Calendar.YEAR, 2023);  
      
      • 示例二:一次性设置年、月、日。需要注意的是,月份是从 0 开始计数的,Calendar.SEPTEMBER代表 8(因为 0 代表 1 月)。此代码将日历设置为 2008 年 9 月 8 日。
      calendar.set(2008, Calendar.SEPTEMBER, 8);
      
    add 方法:日历中各个部分的加减
    • 用于在日历的指定字段上增加或减少一定的数量。正数表示增加,负数表示减少。

      • 示例:将当前日历的年份增加 1 年。
      calendar.add(Calendar.YEAR, 1);
      
    setTime () 方法:让日历关联具体的时间
    • 通过传入一个Date对象,使日历对象关联到该Date对象所表示的时间。

      calendar.setTime(new Date());
      
    getTime () 方法:获取日历的具体时间
    • 该方法会返回一个Date对象,这个对象表示日历所设置的时间。

      Date time = calendar.getTime();
      
Java8的新日期API(第六章ppt35- 41)
获取当前时间(精确到纳秒)

LocalDateTime能够获取包含日期和时间的当前时刻,精确到纳秒。代码如下:

LocalDateTime now = LocalDateTime.now(); 
获取指定日期时间

通过of方法可以创建一个指定日期和时间的LocalDateTime实例。例如:

LocalDateTime ldt = LocalDateTime.of(2008, 8, 8, 8, 8, 8, 8); 
// 上述代码获取了2008年8月8日8时8分8秒8纳秒的指定日期时间
加日期和加时间(实例方法)

LocalDateTime提供了一系列plus开头的实例方法,用于在现有日期时间基础上增加相应的时间量。例如:

LocalDateTime localDateTime = ldt.plusYears(1)
                                .plusMonths(1)
                                .plusDays(1)
                                .plusHours(1)
                                .plusMinutes(1)
                                .plusSeconds(1)
                                .plusNanos(1);
// 上述代码在ldt的基础上,依次增加1年、1个月、1天、1小时、1分钟、1秒和1纳秒
减日期和减时间(实例方法)

同样,LocalDateTime也有minus开头的实例方法,用于减少时间量。示例如下:

LocalDateTime localDateTime = ldt.minusYears(1)
                                .minusMonths(1)
                                .minusDays(1)
                                .minusHours(1)
                                .minusMinutes(1)
                                .minusSeconds(1)
                                .minusNanos(1);
// 上述代码在ldt的基础上,依次减少1年、1个月、1天、1小时、1分钟、1秒和1纳秒
获取年月日时分秒

可以从LocalDateTime实例中获取各个部分的时间信息。示例如下:

int year = now.getYear(); 
// 获取年份

int month = now.getMonth().getValue(); 
// 获取月份,这里通过getValue()方法获取月份的数值表示

int dayOfMonth = now.getDayOfMonth(); 
// 获取一个月中的第几天

int dayOfWeek = now.getDayOfWeek().getValue(); 
// 获取一周中的第几天,星期日为1,星期一是2,以此类推

int dayOfYear = now.getDayOfYear(); 
// 获取一年中的第几天

int hour = now.getHour(); 
// 获取小时

int minute = now.getMinute(); 
// 获取分钟

int second = now.getSecond(); 
// 获取秒

int nano = now.getNano(); 
// 获取纳秒

需要注意的是,LocalDateLocalTime类也有类似的获取部分时间信息和进行时间增减的方法,只不过LocalDate仅针对日期部分,LocalTime仅针对时间部分。例如,LocalDate可以获取年、月、日,进行日期的增减;LocalTime可以获取时、分、秒、纳秒,进行时间的增减 。

数学类Math

  • java.lang.Math 数学工具类,都是静态方法

    • 常用属性: static final double PI (圆周率)

    • 常用方法:

      • static int abs(int a) :求绝对值
      • static double ceil(double a) : 向上取整
      • static double floor(double b) : 向下取整
      • static int max (int a, int b): 取最大值
      • static int min (int a, int b): 取最小值
      • static double random( ): 随机数[0.0 - 1.0) int num = (int) (Math.random() * 100) 可以获取[1~100)的随机数
      • static long round(double a): 四舍五入
      • static double sqrt(double a): 平方根
      • static double pow(double a, double b): a的b 次幂

枚举类(Java5新特性)

枚举类型概述

枚举类型在 Java 中是一种引用数据类型。合理使用枚举类型可以让代码更加清晰、可读性更高,还能有效地避免一些常见的错误。

适用场景

当数据是有限的,并且可以一枚一枚列举出来时,就可以考虑使用枚举类型。此外,枚举类型是类型安全的,它能有效防止使用错误的类型进行赋值。

枚举定义格式

以下是最基本的枚举定义格式:

enum 枚举类型名 {
    枚举值1, 枚举值2, 枚举值3, 枚举值4
}
反编译特性

通过反编译(javap)可以发现枚举类型具有以下特性:

  • 所有枚举类型默认继承 java.lang.Enum,因此枚举类型无法继承其他类。
  • 所有的枚举类型都被 final 修饰,所以枚举类型是无法被继承的。
  • 所有的枚举值都是常量。
  • 所有的枚举类型中都有一个 values 数组,可以通过 values() 方法获取所有枚举值并进行遍历。

枚举的高级用法

可编写元素

普通类中可以编写的元素,在枚举类型中同样可以编写,具体如下:

  • 代码块:静态代码块、构造代码块。
  • 方法:实例方法、静态方法。
  • 变量:实例变量、静态变量。
构造方法特点
  • 枚举类中的构造方法是私有化的,默认就是私有化,只能在本类中调用。
  • 构造方法调用时不能使用 new,直接使用 “枚举值 (实参);” 进行调用。
  • 每一个枚举值相当于枚举类型的实例。
枚举值定义规则

枚举类型中如果编写了其他代码,必须要有枚举值,并且枚举值的定义要放到最上面,最后一个枚举值的分号不能省略。

继承与接口实现情况

枚举类因为默认继承了 java.lang.Enum,因此不能再继承其他类,但可以实现接口,实现方式有以下两种:

  • 第一种实现方式:在枚举类中实现接口。
  • 第二种实现方式:让每一个枚举值实现接口。

随机数Random

java.util.Random 随机数生成器(生成随机数的工具类)

  • 常用的构造方法:
    • Random()
  • 常用的方法:
    • int nextInt(); 获取一个int类型范围内的随机数
    • int nextInt(int bound); 获取一个[0,bound)区间内的随机数
    • double nextDouble(); 获取[0.0,1.0)之间的随机数

java.lang.System 类的常用属性和方法

常用属性

  • static final PrintStream err:标准错误输出流。使用 System.err.println("错误信息"); 输出的内容会显示为红色字体。
  • static final InputStream in:标准输入流。
  • static final PrintStream out:标准输出流。

常用方法

  1. static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
    • 功能:进行数组拷贝。
    • 参数说明:
      • src:源数组。
      • srcPos:源数组的起始位置。
      • dest:目标数组。
      • destPos:目标数组的起始位置。
      • length:要拷贝的元素个数。
  2. static void exit(int status)
    • 功能:退出虚拟机。
    • 参数说明:
      • status:退出状态码,通常 0 表示正常退出,非 0 表示异常退出。
  3. static void gc()
    • 功能:建议启动垃圾回收器。但这只是一个建议,JVM 不一定会立即执行垃圾回收操作。
  4. static long currentTimeMillis()
    • 功能:获取自 1970 - 01 - 01 00:00:00 000 到系统当前时间的总毫秒数。
  5. static long nanoTime()
    • 功能:获取自 1970 年 1 月 1 日 0 时 0 分 0 秒以来,当前时间的纳秒数。
  6. static Map<String, String> getenv()
    • 功能:获取当前系统的环境变量,例如 PathJAVA_HOMECLASSPATH 等。
  7. static Properties getProperties()
    • 功能:获取当前系统的属性。
  8. static String getProperty(String key)
    • 功能:通过 key 获取指定的系统属性。
    • 参数说明:
      • key:系统属性的键名。

UUID

UUID(通用唯一标识符)是一种软件构建的标准,用来生成具有唯一性的ID

Java中的java.util.UUID类提供对UUID的支持:

  • 生成UUID: static UUID randomUUID();
  • 转成字符串: String toString();

第七章 集合

集合概述

  • 什么是集合?有什么用?
    • 集合是一种容器,用来组织和管理数据,非常重要
    • Java的集合框架对应的这套类库其实就是对各种数据结构的实现
    • 每一个集合类底层采用的数据结构不同,例如ArrayList集合底层采用了数组,LinkedList集合底层采用了双向链表,HashMap集合底层采用了哈希表,TreeMap集合底层采用了红黑树
    • 我们不需要写数据结构的实现了,可以直接使用,但我们需要知道的是在哪种场合下选择哪一个集合效率是最高的
  • 集合中存储的是引用,不是把堆中存储的对象存储到集合中,是把对象的内存地址存储到集合中
  • 默认情况下,如果不使用泛型的话,集合中可以存储任何类型的引用,只要是Object的子类都可以存储
  • Java集合相关的类都在Java.util包下
  • Java集合框架分为两部分:
    • Collection结构: 元素以单个形式存储
    • Map结构: 元素以键值对的映射关系存储

Collection的继承结构

  • SequencedCollection和 SequencedSet接口都是在Java21新增的接口
  • 图中蓝色的是实现类,其他的是接口
  • 六个实现类中只有HashSet是无序集合,其余都是有序集合
    • 有序集合: 集合中存储的元素有下标或者集合中存储的元素是可排序的
    • 无序集合: 集合中存储的元素没有下标并且集合中存储的元素也没有排序
  • 每个集合实现类对应的数据结构如下:
    • LinkedList: 双向链表 (不是队列数据结构,但是使用它可以模拟队列)
    • ArrayList: 数组
    • Vector: 数组(线程安全)
    • HashSet: 哈希表
    • LinkedHashSet: 双链表 + 哈希表
    • TreeSet: 红黑树
  • List集合中存储的元素可以重复,Set集合中存储的元素不可重复

Collection接口的通用方法

方法签名方法描述
boolean add(E e);向集合中添加元素
int size();获取集合中元素个数
boolean addAll(Collection c);将参数集合中所有元素全部加入当前集合
boolean contains(Object o);判断集合中是否包含对象 o
boolean remove(Object o);从集合中删除对象 o
void clear();清空集合
boolean isEmpty();判断集合中元素个数是否为 0
Object[] toArray();将集合转换成一维数组

要使用Collection接口的方法必须先创建实现类才能调用

例如: Collection c = new ArrayList(); (多态)

package com.powernode.javase;

import java.util.ArrayList;
import java.util.Collection;

/**
 * Collection接口的通用方法的练习
 */
public class CollectionTest01 {
    public static void main(String[] args) {
        Collection c = new ArrayList();

        c.add(1);
        c.add("库库");
        c.add(3.14);
        c.add(new Object());
        System.out.println(c.size());

        Collection c1 = new ArrayList();
        c1.add(10);
        c1.add("kuku");
        c1.add(12.23);

        c.addAll(c1);
        System.out.println(c.size());

        Object[] array = c.toArray();
        for (Object arr : array){
            System.out.println(arr);
        }

        String s = new String("库库");
        //调用了equals方法来比较
        System.out.println(c.contains(s));//true

        c.remove(s);
        System.out.println(c.size());//也可以删除"库库"元素

        System.out.println(c.isEmpty());
        c.clear();
        System.out.println(c.size());//0  清空元素
        System.out.println(c.isEmpty());
        
    }
}

Collection的遍历(集合的通用遍历方式)

  • 第一步: 获取当前集合依赖的迭代器对象
    • Iterator it = collection.iterator();
  • 第二步: 编写循环,循环条件是: 当前光标所在的位置是否存在元素
    • while(it.hasNext()){}
  • 第三步: 如果有,获取光标所在位置的元素,并指向下一个位置
    • Object obj = it.next();
    • System.out.println(obj);
package com.powernode.javase;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

/**
 * Collection的遍历
 */
public class CollectionTest02 {
    public static void main(String[] args) {
        //Collection c = new ArrayList<>();
        //面向接口编程
        Collection c = new LinkedList();
        c.add(1);
        c.add(3.14);
        c.add(true);
        c.add("kuku");

        /*//获取迭代器对象
        Iterator it = c.iterator();
        System.out.println("迭代器: " + it);

        while(it.hasNext()){
            Object obj = it.next();
            System.out.println(obj);
        }*/

        //for循环也可以
        for (Iterator it = c.iterator();it.hasNext();){
            Object obj = it.next();
            System.out.println(obj);
        }
    }
}

SequencedCollection接口的方法

所有的有序集合都实现了SequencedCollection接口

  • SequencedCollection接口是Java21版本新增的
  • SequencedCollection接口中的方法:
    • addFirst(Object o): 向头部添加
    • addLast(Object o): 向尾部添加
    • removeFirst(): 删除头部
    • removeLast(): 删除尾部
    • getFirst(): 获取头部节点
    • getLast(): 获取尾部节点
    • reversed(): 反转集合中的元素
  • ArrayList, LinkedList, Vector, LinkedHashSet, TreeSet, Stack都可以调用这个接口中的方法
package com.powernode.javase;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.SequencedCollection;

/**
 * 有序集合SequencedCollection的方法
 */
public class SequencedCollectionTest01 {
    public static void main(String[] args) {
        SequencedCollection sc = new ArrayList();
        sc.add(1);
        sc.add(2);
        sc.add(3);
        sc.add(4);

        sc.addFirst(0);
        sc.addLast(5);
        Iterator it = sc.iterator();
        while(it.hasNext()){
            Object obj = it.next();
            System.out.println(obj);
        }
        System.out.println("==================");
        sc.removeFirst();
        sc.removeLast();
        it = sc.iterator();
        while(it.hasNext()){
            Object obj = it.next();
            System.out.println(obj);
        }

        System.out.println(sc.getFirst());
        System.out.println(sc.getLast());

        System.out.println("============");
        SequencedCollection rsc = sc.reversed();
        it = rsc.iterator();
        while(it.hasNext()){
            Object obj = it.next();
            System.out.println(obj);
        }
    }
}

泛型

  • 泛型是java5的新特性,属于编译阶段的功能.
  • 泛型可以让开发者在编写代码时指定集合中存储的数据类型
  • 泛型的作用:
    • 类型安全: 指定了集合中元素的类型后,编译器会在编译时进行类型检查,如果尝试将错误类型的元素添加到集合中,就会在编译时报错,避免了运行时出现类型错误的问题
    • 代码简洁: 使用泛型可以简化代码, 避免了繁琐的类型转换操作. 比如,在没有使用泛型的时候,需要使用Object类型来保存集合中的元素,并在使用时需要强制类型转换成实际类型,而有了泛型之后,只需要在定义集合时指定元素类型就行
  • 在集合中使用泛型
    • Collection strs = new ArrayList();
    • 这就表示该集合中只能存储字符串,存储其他类型时编译器报错.
    • 并且以上代码使用泛型后,避免了繁琐的类型转换,集合中的元素可以直接调用String类特有的方法
  • Java7的新特性: 钻石表达式
    • Collection strs = new ArrayList<>();

泛型的擦除和补偿(了解)

  • 泛型的出现提高了编译时的安全性。正因为编译时对添加的数据做了检查,程序运行时才不会抛出类型转换异常。因此,泛型本质上是编译时期的技术,是专门给编译器用的。
  • 加载类的时候,会将泛型擦除掉(擦除之后的类型为 Object 类型),这个称为泛型擦除。
  • 为什么要有泛型擦除呢?
  • 其本质是为了让 JDK1.4 和 JDK1.5 能够兼容同一个类加载器。在 JDK1.5 版本中,程序编译时期会对集合添加的元素进行安全检查。如果检查完是安全的、没有错误的,那么就意味着添加的元素都属于同一种数据类型。此时,加载类时就可以把这个泛型擦除掉,将泛型擦除后的类型就是 Object 类,这样擦除之后的代码就与 JDK1.4 的代码一致。
  • 由于加载类的时候,会默认将类中的泛型擦除为 Object 类型,所以添加的元素就被转化为 Object 类型,同时取出的元素也默认为 Object 类型。
  • 而我们获得集合中的元素时,按理说取出的元素应该是 Object 类型,为什么取出的元素却是实际添加的元素类型呢?
  • 这里又做了一个默认的操作,我们称之为泛型的补偿。在程序运行时,通过获取元素的实际类型进行强转,这就叫做泛型补偿(不必手动实现强制转换)。获得集合中的元素时,虚拟机会根据获得元素的实际类型进行向下转型,也就是会恢复获得元素的实际类型,因此我们就无需手动执行向下转型操作,从本质上避免了抛出类型转换异常。

泛型的使用

在类上定义泛型

语法:class 类名<泛型1,泛型2,泛型3…>{}

package com.powernode.javase;

/**
 * 在类上自定义泛型
 */
public class MyClass<T> {
    private T name;
    private T age;

    public MyClass(T name, T age) {
        this.name = name;
        this.age = age;
    }

    public T getName() {
        return name;
    }

    public void setName(T name) {
        this.name = name;
    }

    public T getAge() {
        return age;
    }

    public void setAge(T age) {
        this.age = age;
    }

    public static void main(String[] args) {
        MyClass<String> stringMyClass = new MyClass<>("kuku","20");
        System.out.println(stringMyClass.getName());
        System.out.println(stringMyClass.getAge());

        MyClass<Integer> integerMyClass = new MyClass<>(230, 20);
        System.out.println(integerMyClass.getName());
        System.out.println(integerMyClass.getAge());
    }
}

在静态方法上自定义泛型

  • 在类上定义的泛型,在静态方法中无法使用。如果在静态方法中使用泛型,则需要在方法返回值类型前面进行泛型的声明。
  • 语法格式:<泛型1, 泛型2, 泛型3, …> 返回值类型 方法名(形参列表) {}
package com.powernode.javase;

public class MyStatic {
    public static <E> void print(E[] elts){
        for (E elt : elts){
            System.out.println(elt);
        }
    }

    public static void main(String[] args) {
        String[] strs = {"zhangsan","lisi"};
        MyStatic.print(strs);
    }
}

在接口上自定义泛型

  • 语法格式:interface 接口名<泛型1,泛型2,…> {}
  • 例如:public interface Flayable{}
  • 实现接口时,如果知道具体的类型,则:public class MyClass implements Flyable{}
  • 实现接口时,如果不知道具体的类型,则:public class MyClass implements Flyable{}

泛型的通配符

  • 泛型是在限定数据类型,当在集合或者其他地方使用到泛型后,那么这时一旦明确泛型的数据类型,那么在使用的时候只能传递和数据类型匹配的类型,否则就会报错
  • 有的情况下,我们在定义Java方法时,根本无法确定集合中存储的元素类型是什么.为了解决这个"无法确定集合中存储元素类型的问题",Java语言就提供了通配符
  • 通配符的几种形式:
    • 无限定通配符: <?>, 此处的 “?” 可以为任意引用数据类型
    • 上限通配符: <? extends Number>, 此处"?"必须为Number及其子类
    • 下限通配符: <? super Number>, 此处"?"必须为Number及其父类

迭代时删除元素

一、迭代集合时删除元素的两种方式及结果

  1. 使用 “集合对象.remove (元素)”:会出现ConcurrentModificationException (并发修改)异常。
  2. 使用 “迭代器对象.remove ()”:不会出现异常。

二、关于集合的并发修改问题

想象存在两个线程:A 和 B。A 线程负责迭代遍历集合,B 线程负责删除集合中的某个元素。当这两个线程同时执行时,就会产生并发修改的问题。

三、解决并发修改问题的方法 ——fail-fast 机制

  1. 定义:fail-fast 机制又被称为快速失败机制。也就是说,只要程序发现了对集合进行了并发修改,就会立即让其失败,以防出现错误。
  2. 实现原理:
    • 集合中设置了一个modCount属性,用来记录修改次数。当使用集合对象执行增、删、改中任意一个操作时,modCount就会自动加 1。
    • 获取迭代器对象的时候,会给迭代器对象初始化一个expectedModCount属性。并且将expectedModCount初始化为modCount,即:int expectedModCount = modCount;
    • 当使用集合对象删除元素时:modCount会加 1,但是迭代器中的expectedModCount不会加 1。而当迭代器对象的next()方法执行时,会检测expectedModCountmodCount是否相等,如果不相等,则抛出ConcurrentModificationException异常。
    • 当使用迭代器删除元素的时候:modCount会加 1,并且expectedModCount也会加 1。这样当迭代器对象的next()方法执行时,检测到的expectedModCountmodCount相等,则不会出现ConcurrentModificationException异常。

四、注意事项

  1. 虽然我们当前写的程序是单线程的程序,并没有使用多线程,但是通过迭代器去遍历的同时使用集合去删除元素,这个行为将被认定为并发修改。
  2. 结论:迭代集合时,删除元素要使用 “迭代器对象.remove ()” 方法来删除,避免使用 “集合对象.remove (元素)”,主要是为了避免ConcurrentModificationException异常的发生。
  3. 迭代器的remove()方法删除的是next()方法的返回的那个数据。remove()方法调用之前一定是先调用了next()方法,如果不是这样的,就会报错。

List接口的常用方法

一、List 集合存储元素特点

  • 有序
    • List 集合中的元素都是有下标的,从 0 开始,以 1 递增。
  • 可重复
    • 存进去 1,还可以再存一个 1。

二、List 接口下常见的实现类

  • ArrayList:基于数组实现。
  • Vector、Stack:基于数组实现,并且是线程安全的。
  • LinkedList:基于双向链表实现。

三、List 接口特有方法

以下是在 Collection 和 SequencedCollection 中没有的方法,只适合 List 家族使用,这些方法都和下标有关系:

  • void add(int index, E element):在指定索引处插入元素。
  • E set(int index, E element):修改索引处的元素。
  • E get(int index):根据索引获取元素。通过这个方法,List 集合具有自己特殊的遍历方式:根据下标遍历。
  • E remove(int index):删除索引处的元素。
  • int indexOf(Object o):获取对象 o 在当前集合中第一次出现时的索引。
  • int lastIndexOf(Object o):获取对象 o 在当前集合中最后一次出现时的索引。
  • List<E> subList(int fromIndex, int toIndex):截取子 List 集合生成一个新集合,范围是 [fromIndex, toIndex),对原集合无影响。
  • static List<E> of(E... elements):静态方法,返回包含任意数量元素的不可修改列表,获取的集合是只读的,不可修改。

四、List接口使用comparator排序 (传入的是一个比较器)

回顾数组中自定义类型是如何排序的?

  • 所有自定义类型排序时必须指定排序规则. (int不需要指定,String不需要指定,因为他们都有固定的排序规则,int按照数字大小,String按照字典中的顺序)

  • 如何给自定义类型指定排序规则? 让自定义类型实现java.lang.Comparator接口,然后重写compareTo方法,在该方法中指定排序规则

  • List集合的排序

    • default void sort(Comparator<? super E> c); 对List集合中元素排序可以调用此方法。
    • sort方法需要一个参数: java.util.Comparator。我们把这个参数叫做比较器。这是一个接口。
    • 如何给自定义类型指定比较规则?可以对Comparator提供一个实现类,并重写compare方法来指定比较规则。
    • 当然,Comparator接口的实现类也可以采用匿名内部类的方式。

ArrayList 概述

ArrayList 集合底层采用了数组这种数据结构。

优点 : 底层是数组,因此根据下标查找元素的时间复杂度是 O (1),检索效率高。

缺点 : 随机增删元素效率较低。不过只要数组的容量还没满,对末尾元素进行增删,效率不受影响。

适用场景

需要频繁地检索元素,并且很少进行随机增删元素时建议使用。

默认初始化容量

从源码角度可以看到,当调用无参数构造方法时,初始化容量为 0,当第一次调用 add 方法时将 ArrayList 容量初始化为 10 个长度。

扩容策略

底层扩容会创建一个新的数组,然后使用数组拷贝。扩容之后的新容量是原容量的 1.5 倍。

使用 ArrayList 集合时,最好预测大概数量并给定初始化容量,这样可以减少扩容次数。

方法操作分析

  • 添加元素:涉及元素添加到集合的具体逻辑,可能会触发扩容机制。 .add()
  • 修改元素:根据索引对集合中已有的元素进行修改。 .set(index, obj)
  • 插入元素:在指定位置插入新元素,插入操作可能会涉及到元素的移动。.add(index, obj)
  • 删除元素:移除指定位置或指定元素,删除操作可能会涉及到元素的移动以填补空缺位置。 .remove(index)

Vector概述

  • Vector底层也是数组,和ArrayList相同。
  • 不同的是Vector几乎所有的方法都是线程同步的(被synchronized修饰:线程排队执行,不能并发),因此Vector是线程安全的,但由于效率较低,很少使用。因为控制线程安全有新方式。
  • Vector初始化容量:10
  • Vector扩容策略:扩容之后的容量是原容量的2倍。

链表概述

链表类型

  1. 单向链表:由节点组成,每个节点包含数据和指向下一个节点的指针,只能从前往后遍历。

  2. 双向链表:节点除了包含数据,还具有指向前一个节点和后一个节点的指针,可双向遍历。

  3. 环形链表

    :分为环形单链表和环形双链表。

    • 环形单链表:单向链表的尾节点指针指向头节点,形成一个环。
    • 环形双链表:双向链表的头节点的前驱指针指向尾节点,尾节点的后继指针指向头节点,构成环形结构。

链表优点

链表节点在空间存储上内存地址不连续。在删除某个节点时,无需进行元素位移操作,因此随机增删元素效率较高,时间复杂度为 O (1)。

链表缺点

由于链表节点的内存地址在空间上不连续,在查找元素时,只能从某个节点开始顺序查找,导致查找元素效率较低,时间复杂度为 O (n)。

链表的适用场景

适用于需要频繁进行随机增删操作,但查找操作较少的场景。

*手写单向链表

package com.powernode.javase;

//定义结点类
class Node{
    int data;
    Node next;

    public Node(int data) {
        this.data = data;
        this.next = null;
    }
}

//定义单向列表类
class SinglyLinkedList{
    private Node head;//头结点

    @Override
    public String toString() {
        return super.toString();
    }

    public SinglyLinkedList() {
        this.head = null;
    }

    //添加结点到列表末尾
    public void add(int data){
        Node newNode = new Node(data);
        if (head == null){
           head = newNode;
        }else {
            //current类似于一个指针结点
            Node current = head;
            while(current.next != null){
                current = current.next;
            }
            current.next = newNode;
        }
    }

    public void update(int index, int data){
        Node current = head;
        int count = 0;
        while(current != null) {
            if (count == index) {
                current.data = data;
                return;
            }
            current = current.next;
            count++;
        }
        throw new IndexOutOfBoundsException("Index not found");
    }

    public void delete(int index){
        //判断列表中是否有元素
        if (head == null){
            throw new IndexOutOfBoundsException("List is empty");
        }
        if (index == 0){
            head = head.next;
            return;
        }
        //创建两个类似指针的结点
        Node current = head;
        Node previous = null;
        int count = 0;
        while(current != null){
            if (count == index){
                previous.next = current.next;
                return;
            }
            previous = current;
            current = current.next;
            count++;
        }
        throw new IndexOutOfBoundsException("Index not found");
    }
    public void printf(){
        Node current = head;

        while (current != null){
            System.out.print(current.data + " -> ");
            current = current.next;
        }
        System.out.print("null");
    }

}


public class Main {
    public static void main(String[] args) {
        SinglyLinkedList sll = new SinglyLinkedList();

        sll.add(1);
        sll.add(2);
        sll.add(3);
        sll.add(4);
        sll.printf();
        System.out.println();

        sll.update(1,10);
        sll.printf();
        System.out.println();

        sll.delete(1);
        sll.printf();
    }
}

栈数据结构

  • LIFO原则(Last in, First out): 后进先出原则

  • 实现栈数据结构: 可以用数据结构也可以用双向链表来实现

  • 用数组实现的代表是:Stack、ArrayDeque

  • Stack:Vetor的子类,实现了栈数据结构,除了具有Vetor的方法,还扩展了其它方法,完成了栈结构的模拟。不过在JDK1.6(Java6)之后就不建议使用了,因为它是线程安全的,太慢了。Stack中的方法如下:

    • E push(E item):压栈
    • E pop():弹栈(将栈顶元素删除,并返回被删除的引用)
    • int search(Object o):查找栈中元素(返回值的意思是:以1为开始,从栈顶往下数第几个)
    • E peek():窥视栈顶元素(不会将栈顶元素删除,只是看看栈顶元素是什么。注意:如果栈为空时会报异常。)
  • ArrayDeque

    • E push(E item)
    • E pop()
  • 用链表实现的代表是:LinkedList

  • LinkedList

    • E push(E item)
    • E pop()

队列

队列里入队和出队方法为: offer() 和 poll()

ArrayDeque和LinkedList可以模拟双端队列

环形数组底层原理

ArrayDeque模拟双端队列

Map继承结构

Map 集合概述

  • Map 集合以键值对(key - value)形式存储数据,其中 key 和 value 存储的均为引用。
  • 在 Map 集合里,key 起主导作用,value 依附于 key。

不同 Map 集合的特性

有序集合
  • SequencedMap:Java 21 新增的集合类型。
  • LinkedHashMap:能保证 key 的有序性,维持插入顺序。
  • TreeMap:key 是有序的,可实现排序功能。
无序集合
  • HashMap:key 是无序的,底层采用哈希表 / 散列表数据结构。
  • Hashtable:key 无序,与 HashMap 类似,但它是线程安全的。
  • Properties:是 Hashtable 的子类,key 和 value 都为字符串类型,常用于处理配置文件,key 无序。

Map 集合的 key 特性

  • Map 集合的 key 具有不可重复性。若出现重复的 key,新的 value 会覆盖旧的 value。

Set 集合与 Map 集合的关系

HashSet

  • 底层实现:底层创建了一个 HashMap 实例。
  • 存储机制:向 HashSet 集合中存储元素时,实际上是将元素存于 HashMap 集合的 key 部分。
  • 特性:由于 HashMap 的 key 无序且不可重复,所以 HashSet 也是无序且不可重复的,其底层同样是哈希表 / 散列表。

TreeSet

  • 底层实现:底层使用了一个 TreeMap 实例。
  • 存储机制:往 TreeSet 集合中存储元素时,实际上是将元素存于 TreeMap 集合的 key 部分。
  • 特性:因为 TreeMap 的 key 不可重复且可排序,所以 TreeSet 也是不可重复但可排序的,其底层为红黑树,排序可通过 java.lang.Comparablejava.util.Comparator 实现。

LinkedHashSet

  • 底层实现:底层基于 LinkedHashMap 实现。
  • 特性:LinkedHashMap 能保证元素的插入顺序,不过效率低于 HashSet,底层采用哈希表 + 双向链表实现。

Set 集合添加元素的底层逻辑

从源码可知,向 Set 集合中调用 add 方法时,底层会调用 Map 的 put 方法。这里的 value 是一个固定不变的常量,仅作为占位符,主要起作用的是 key。

Map集合的遍历方式

package com.powernode.javase;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class MapTest01 {
    public static void main(String[] args) {
        Map<Integer,String> maps = new HashMap<>();

        //添加元素
        maps.put(1,"张三");
        maps.put(2,"李四");
        maps.put(3,"王五");
        maps.put(4,"赵六");
        maps.put(5,"钱七");


        /*//遍历map集合
//        首先获取所有的key,然后通过maps.get(key)方法获取所有的value
        *//*Set<Integer> keys = maps.keySet();//将所有的key返回一个set集合
        Iterator<Integer> it = keys.iterator();//创建一个keys集合的迭代器
        while(it.hasNext()){
            Integer key= it.next();
            String value = maps.get(key);
            System.out.println(key + " -> " + value);
        }*//*

        //第二种写法
        Set<Integer> keys = maps.keySet();
        for (Integer key : keys){
            System.out.println(key + " -> " + maps.get(key));
        }*/

        //使用Entry遍历map集合
        /*Set<Map.Entry<Integer, String>> entries = maps.entrySet();
        Iterator<Map.Entry<Integer, String>> it = entries.iterator();
        while(it.hasNext()){
            Map.Entry<Integer, String> next = it.next();
            Integer key = next.getKey();
            String value = next.getValue();
            System.out.println(key + " -> " + value);
        }*/
        Set<Map.Entry<Integer, String>> entries = maps.entrySet();
        for (Map.Entry<Integer, String> entry : entries){
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

    }
}

Map接口的常用方法

  1. put 方法:用于添加或更新键值对。若 key 不存在则添加新的键值对,若 key 已存在则更新对应的 value,并返回旧的 value
  2. putAll 方法:将另一个 Map 中的所有键值对添加到当前 Map 中。
  3. get 方法:根据 key 获取对应的 value,若 key 不存在则返回 null
  4. containsKey 方法:检查 Map 中是否包含指定的 key
  5. containsValue 方法:检查 Map 中是否包含指定的 value
  6. remove 方法:根据 key 删除对应的键值对,并返回被删除的 value,若 key 不存在则返回 null
  7. clear 方法:清空 Map 中的所有键值对。
  8. size 方法:返回 Map 中键值对的数量。
  9. isEmpty 方法:判断 Map 是否为空。
  10. values 方法:返回一个包含 Map 中所有 valueCollection
  11. keySet 方法:返回一个包含 Map 中所有 keySet
  12. entrySet 方法:返回一个包含 Map 中所有键值对的 Set,每个元素是一个 Map.Entry 对象。
  13. of 方法:这是一个静态方法,用于创建一个不可变的 Map,最多可接受 10 个键值对。尝试修改不可变 Map 会抛出 UnsupportedOperationException

HashMap

HashMap 概述

  • key 的特性:
    • 无序性:HashMap 集合中 key 的插入顺序和取出顺序不一定相同。例如,依次插入 “a”、“b”、“c”,取出时顺序可能发生变化。
    • 不可重复性:key 具有唯一性。这意味着在 HashMap 中,每个 key 都是独一无二的。
  • put 操作特性:当向 HashMap 集合中执行 put 操作时,如果 key 重复,新的 value 会覆盖旧的 value。例如,先执行 map.put("key1", "value1"),再执行 map.put("key1", "value2"),此时 map.get("key1") 返回的将是 “value2”。

插入自定义类型作为 key 的情况

HashMap 集合的 key 具有唯一性。当向 key 部分插入自定义的类型时,情况较为特殊。在判断自定义类型的 key 是否相等时,HashMap 会依赖自定义类型的 hashCode 和 equals 方法。

自定义类型重写 equals 方法之后的情况

如果自定义的类型重写了 equals 方法,却没有同时重写 hashCode 方法,那么可能会导致在 HashMap 中出现意外行为。因为 HashMap 首先会根据 key 的 hashCode 值来确定其在哈希表中的存储位置,然后再通过 equals 方法判断是否为同一个 key。如果两个自定义对象通过 equals 方法判断相等,但 hashCode 不同,它们可能会被存储在哈希表的不同位置,破坏了 key 的唯一性原则。所以,当重写 equals 方法时,通常也需要重写 hashCode 方法,并且要保证两个通过 equals 方法判断相等的对象,其 hashCode 值也相等。

HashMap遍历方式(Map的遍历方式)

Map<String, Integer> map = new HashMap<>();
        map.put("apple", 1);
        map.put("banana", 2);
        map.put("cherry", 3);

        // 方式一:使用 entrySet 遍历
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
        }

        // 方式二:使用 keySet 遍历
        for (String key : map.keySet()) {
            System.out.println("Key: " + key + ", Value: " + map.get(key));
        }

HashMap 底层数据结构 —— 哈希表

  • 哈希表的重要性:哈希表是一种查询和增删效率都很高的数据结构,在众多场景中广泛应用,在面试中也极为常见,是开发者必须掌握的知识要点。

  • 哈希表的结构组成

    :哈希表是 “数组 + 链表” 的结合体,但这种结合并非固定不变。具体来说,哈希表可能呈现以下几种结构形式:

    • 数组 + 链表:这是哈希表较基础的结构形式。通过 key 的 hashCode 值计算出在数组中的索引位置,若该位置已有元素(即发生哈希冲突),则以链表形式将新元素挂在该位置。
    • 数组 + 红黑树:当链表长度达到一定阈值(如在 HashMap 中默认是 8)时,链表会转换为红黑树,以提升查询效率。红黑树作为一种自平衡二叉查找树,其查询、插入和删除操作的时间复杂度为 O(logn),比链表的 O(n) 效率更高。
    • 数组 + 链表 + 红黑树:在实际应用中,HashMap 会依据链表长度动态地在链表和红黑树之间转换。当链表长度小于一定阈值(如在 HashMap 中默认是 6)时,红黑树又会转换回链表。这种动态转换机制能在不同情况下充分发挥链表和红黑树的优势,保障 HashMap 的高效查询和增删操作。

哈希表存储原理

  1. 概念
    • 哈希表:是一种数据结构的名称。它能够高效地存储和检索数据,在众多场景中发挥关键作用。
  2. 哈希函数
    • 通过哈希函数,可以将一个 Java 对象映射为一个数字。这就如同在现实世界里,每一个人(对象)都会对应一个身份证号(哈希值)。
    • 在 Java 中,hashCode()方法就充当了哈希函数的角色。也就是说,hashCode()方法的返回值即为哈希值。
    • 一个优质的哈希函数,能够使散列分布均匀,尽可能减少后续的哈希冲突。
  3. 哈希值
    • 哈希值,也被称作哈希码,它是哈希函数执行后的结果。哈希值在哈希表的存储和查找过程中起到重要的标识作用。
  4. 哈希碰撞(哈希冲突)
    • 当两个对象经过 “哈希值 % 数组长度” 运算之后,得到的下标相同,此时就发生了哈希冲突。例如,对象 A 和对象 B 的哈希值分别为 10 和 20,而哈希表数组长度为 5,那么 (10 % 5) 和 (20 % 5) 都等于 0,这就产生了哈希冲突。
    • 解决哈希冲突的方法:通常是将发生冲突的元素挂到同一个链表上,或者在冲突元素较多时,将链表转换为同一个红黑树来存储。在 Java 的HashMap中,当链表长度达到一定阈值(默认为 8)时,链表会转换为红黑树,以提升查询效率;当链表长度小于一定阈值(默认为 6)时,红黑树又会转换回链表。
  5. 重点强调
    • 对于存放在HashMap集合key部分的元素,必须同时重写hashCodeequals方法。这是因为HashMap在存储和检索元素时,会先依据hashCode值来确定元素在哈希表中的大致存储位置,然后再通过equals方法来精确判断元素是否相等。
    • equals方法返回true时,对应的两个对象的hashCode值必须相同。这是 Java 中关于hashCodeequals方法重写的重要约定,遵循此约定才能保证HashMap等基于哈希表的数据结构正常工作。若违背此约定,可能会导致元素存储和检索出现异常,例如,可能会将本应相等的元素视为不同元素存储在HashMap中,或者无法正确检索到已存储的元素。

以上描述中,凡是 “哈希” 都可以替换为 “散列”,二者含义相同 。

HashMap中的key值可以是null,如果存在多个key值为null的情况,则会出现value值的覆盖,默认存储到数组的0号位置.

手写HashMap的put()方法和get()方法

手写HashMap的put方法

【第一步】:处理key为null的情况
如果添加键值对的key就是null,则将该键值对存储到table数组索引为0的位置。
【第二步】:获得key对象的哈希值
如果添加键值对的key不是null,则就调用key的hashcode()方法,获得key的哈希值。
【第三步】:获得键值对的存储位置
因为获得的哈希值在数组合法索引范围之外,因此我们就需要将获得的哈希值转化为[0,数组长度-1]范围的整数,
那么可以通过取模法来实现,也就是通过“哈希值 % 数组长度”来获得索引位置(i)。
【第四步】:将键值对添加到table数组中
当table[i]返回结果为null时,则键键值对封装为Node对象并存入到table[i]的位置。
当table[i]返回结果不为null时,则意味着table[i]存储的是单链表。我们首先遍历单链表,如果遍历出来节点的
key和添加键值对的key相同,那么就执行覆盖操作;如果遍历出来节点的key和添加键值对的key都不同,则就将键键
值对封装为Node对象并插入到单链表末尾。

手写HashMap的get方法

【第一步】:处理key为null的情况
如果查询的key就是null,则就在table数组索引为0的位置去查询。
【第二步】:获得key对象的哈希值
如果查询的key不是null,则就调用key的hashcode()方法,获得key的哈希值。
【第三步】:获得键值对的存储位置
因为获得的哈希值在数组合法索引范围之外,因此我们就需要将获得的哈希值转化为[0,数组长度-1]范围的整数,
那么可以通过取模法来实现,也就是通过“哈希值 % 数组长度”来获得索引位置(i)。
【第四步】:遍历单链表,根据key获得value值
如果table[i]返回的结果为null,则证明单链表不存在,那么返回null即可
如果table[i]返回的结果不为null时,则证明单链表存在,那么就遍历整个单链表。如果遍历出来节点的key和查询
的key相同,那么就返回遍历出来节点的value值;如果整个单链表遍历完毕,则遍历出来节点的key和查询的key都不
相等,那么就证明查询key在链表中不存在,则直接返回null即可。

package com.powernode.javase.hashmap;

import java.security.PrivateKey;

public class MyHashMap<K,V> {
    /**
     * 哈希表
     */
    private Node<K,V>[] table;

    /**
     * 键值对的个数
     */
    private int size;

    public MyHashMap() {
        this.table = new Node[16];
    }

    static class Node<K,V>{
        /**
         * key的hashCode()的散列值的返回值
         */
        int hash;
        /**
         * key
         */
        K key;
        /**
         * value
         */
        V value;
        /**
         * 下一个结点的内存地址
         */
        Node<K,V> next;

        /**
         * 构造一个结点对象
         * @param hash  哈希值
         * @param key   key
         * @param value  value
         * @param next  下一个结点的内存地址
         */
        public Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public String toString(){
            return key + "-> " + value;
        }
    }
/**
 * 手写HashMap的put方法
 * 【第一步】:处理key为null的情况
 *      如果添加键值对的key就是null,则将该键值对存储到table数组索引为0的位置。
 * 【第二步】:获得key对象的哈希值
 *      如果添加键值对的key不是null,则就调用key的hashcode()方法,获得key的哈希值。
 * 【第三步】:获得键值对的存储位置
 *      因为获得的哈希值在数组合法索引范围之外,因此我们就需要将获得的哈希值转化为[0,数组长度-1]范围的整数,
 *      那么可以通过取模法来实现,也就是通过“哈希值 % 数组长度”来获得索引位置(i)。
 * 【第四步】:将键值对添加到table数组中
 *      当table[i]返回结果为null时,则键键值对封装为Node对象并存入到table[i]的位置。
 *      当table[i]返回结果不为null时,则意味着table[i]存储的是单链表。我们首先遍历单链表,如果遍历出来节点的
 *      key和添加键值对的key相同,那么就执行覆盖操作;如果遍历出来节点的key和添加键值对的key都不同,则就将键键
 *      值对封装为Node对象并插入到单链表末尾。
 */
    public V put(K key, V value){
        if (key == null){
            return forNullKey(value);
        }
        //程序执行到这里说明key不为null
        //获取哈希值
        int hash = key.hashCode();
        //通过取模运算计算结点存储在数组的位置
        int index = Math.abs(hash % table.length);
        Node<K,V> node = table[index];
        if (null == node){
            table[index] = new Node<>(hash,key,value,null);
            size++;
            return value;
        }
        //程序执行到这里说明索引处的数组不为空
        Node<K,V> prev = null;
        while (node != null){
            if (node.key.equals(key)){
                V oldValue = node.value;
                node.value = value;
                return oldValue;
            }
            prev = node;
            node = node.next;
        }
        prev.next = new Node<>(hash,key,value,null);
        size++;
        return value;
    }

    //定义一个用于处理当key为null时的方法
    public V forNullKey(V value){
        Node<K,V> node = table[0];
        if (node == null){
            table[0] = new Node<>(0,null,value,null);
            size++;
            return value;
        }
        //程序执行到这里说明头结点不是空
        Node<K,V> prev = null;
        while(node != null){
            if (node.key == null){
                V oldValue = node.value;
                node.value = value;
                return oldValue;
            }
            prev = node;
            node = node.next;
        }
        prev.next = new Node<>(0,null,value,null);
        size++;
        return value;
    }

    /**
     * 手写HashMap的get方法
     * 【第一步】:处理key为null的情况
     *      如果查询的key就是null,则就在table数组索引为0的位置去查询。
     * 【第二步】:获得key对象的哈希值
     *      如果查询的key不是null,则就调用key的hashcode()方法,获得key的哈希值。
     * 【第三步】:获得键值对的存储位置
     *      因为获得的哈希值在数组合法索引范围之外,因此我们就需要将获得的哈希值转化为[0,数组长度-1]范围的整数,
     *      那么可以通过取模法来实现,也就是通过“哈希值 % 数组长度”来获得索引位置(i)。
     * 【第四步】:遍历单链表,根据key获得value值
     *      如果table[i]返回的结果为null,则证明单链表不存在,那么返回null即可
     *      如果table[i]返回的结果不为null时,则证明单链表存在,那么就遍历整个单链表。如果遍历出来节点的key和查询
     *      的key相同,那么就返回遍历出来节点的value值;如果整个单链表遍历完毕,则遍历出来节点的key和查询的key都不
     *      相等,那么就证明查询key在链表中不存在,则直接返回null即可。
     */
    public V get(K key){
        //首先当key==null时
        if (key == null){
            Node<K,V> node = table[0];
            if (node == null){
                return null;
            }
            //程序执行到这里说明下标为0的列表位置有结点
            while(node != null){
                if (node.key == null){
                    return node.value;
                }
                node = node.next;
            }
            //程序执行到这里说明table[0]中没有key为null的结点
            return null;
        }
        //程序执行到这里说明key的值不是null
        //计算哈希值
        int hash = key.hashCode();
        int index = Math.abs(hash % table.length);
        Node<K,V> node = table[index];
        if (node == null){
            return null;
        }
        //程序执行到这里说明哈希表index处有结点
        while(node != null){
            if (node.key.equals(key)){
                return node.value;
            }
            node = node.next;
        }
        //程序执行到这里说明哈希表index处没有key结点
        return null;
    }

@Override
    public String toString(){
        //String str = new String();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < table.length; i++){
            Node<K,V> node = table[i];
            while (null != node){
                /*str += node.toString();
                str += "\n";*/
                sb.append(node);
                sb.append("\n");
                node = node.next;
            }
        }
        return sb.toString();
    }
}

HashMap在Java8之后的改进

  • 初始化时机:
    • java8之前: 构造方法执行时初始化table数组
    • java8之后: 第一次调用put()方法时初始化table数组
  • 插入法:
    • java8之前: 头插法
    • java8之后: 尾插法
  • 数据结构:
    • java8之前: 数组 + 单向链表
    • java8之后: 数组 + 单向链表 + 红黑树
    • 最开始使用单向链表解决哈希冲突. 如果结点数量 >= 8 并且 table数组长度 >= 64, 则单向链表转换成红黑树
    • 当删除红黑树上的结点时, 结点数量 <= 6时,红黑树转换成单向链表

HashMap初始化容量永远都是2的次幂

  1. 哈希值的高效映射

HashMap在存储元素时,要依据键的哈希值来确定元素在数组中的位置。为了达成这个目的,一般会用取模运算(hash % capacity)。不过,取模运算的效率并不高。当capacity为 2 的次幂时,hash % capacity 能够被替换成 hash & (capacity - 1),而位运算的效率比取模运算要高很多。

hash_value % capacityhash_value & (capacity - 1) 的结果是相同的,但位运算的速度更快。

2.均匀分布元素

使用2的次幂作为容量可以使元素更加均匀的分布到数组中.当capacity的容量为2的次幂时,capacity - 1的二进制形式全部都是1,这样一来,hash & capacity - 1的值就只与hash值的低位有关,从而大大减少了哈希冲突的概率

例如,若capacity为 16,capacity - 1的二进制是0000 1111,无论hash值的高位是什么,hash & (capacity - 1) 的结果都只取决于hash值的低 4 位。

3.扩容机制的方便性

HashMap在扩容时,容量变成了原来的2倍,也是2的次幂,重新计算元素的位置也会更加简便,因为要不是原来的位置要不就是原来的位置加上旧的容量.

比如,旧容量是 16,扩容后变为 32。对于某个元素,其旧位置是hash & 15,新位置是hash & 31。由于 31 的二进制是0001 1111,比 15 的二进制0000 1111多了一位,所以新位置要么和旧位置相同,要么就是旧位置加上 16。

综上所述,HashMap将初始化容量设定为 2 的次幂,是为了提升哈希计算的效率、让元素均匀分布以及方便扩容操作。

关于HashMap的初始化容量的设置

  • 默认初始化容量

HashMap默认的初始化容量是 16,代码如下:

import java.util.HashMap;

public class DefaultCapacityExample {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        // 未指定容量,使用默认容量16
        System.out.println("默认初始化容量下的Map:" + map);
    }
}

在上述代码中,创建HashMap时未指定容量,此时使用默认的初始化容量 16。

  • 自定义初始化容量

当你能预估HashMap要存储的元素数量时,可自定义初始化容量,避免多次扩容。不过要注意,HashMap会把你指定的容量转换为大于等于该值的最小 2 的次幂。代码示例如下:

import java.util.HashMap;

public class CustomCapacityExample {
    public static void main(String[] args) {
        // 自定义初始化容量为20,实际会调整为32(大于20的最小2的次幂)
        HashMap<String, Integer> map = new HashMap<>(20); 
        map.put("apple", 1);
        map.put("banana", 2);
        System.out.println("自定义初始化容量下的Map:" + map);
    }
}

在上述代码中,指定初始化容量为 20,实际上HashMap会将其调整为大于 20 的最小 2 的次幂,也就是 32。

  • 初始化容量与负载因子

HashMap还有一个重要的参数是负载因子,默认值是 0.75。当HashMap中的元素数量超过容量 * 负载因子时,就会进行扩容操作。例如:

import java.util.HashMap;

public class LoadFactorExample {
    public static void main(String[] args) {
        // 自定义初始化容量为4,负载因子为0.5
        HashMap<String, Integer> map = new HashMap<>(4, 0.5f); 
        map.put("cat", 3);
        map.put("dog", 4);
        // 此时元素数量达到容量 * 负载因子,会触发扩容
        System.out.println("自定义负载因子下的Map:" + map);
    }
}

在上述代码中,指定初始化容量为 4,负载因子为 0.5,当元素数量达到 2(4 * 0.5)时,就会触发扩容操作。

  • 初始化容量的设置建议
    • 若能预估元素数量:可将初始化容量设置为 (预估元素数量 / 负载因子) + 1,这样能减少扩容次数。例如,预估要存储 100 个元素,负载因子使用默认的 0.75,那么初始化容量可设为 (100 / 0.75) + 1,约为 134,HashMap会将其调整为大于 134 的最小 2 的次幂,即 256。
    • 若无法预估元素数量:使用默认初始化容量即可,HashMap会自动进行扩容操作。

综上所述,合理设置HashMap的初始化容量和负载因子,能提升其性能和效率。

LinkedHashMap概述

  • LinkedHashMap集合和HashMap集合的使用方法完全相同
  • 不过LinkedHashMap可以保证插入顺序
  • LinkedHashMap因为可以保证插入顺序,所以效率比HashMap低一些
  • LinkedHashMap是如何保证插入顺序的? 底层采用了双向链表来记录顺序
  • LinkedHashMap底层采用的是双向链表 + 哈希表的数据结构
  • LinkedHashMap中的key是: 有序不可重复的. 自定义类型的key部分也需要同时重写hashCode()和equals()方法
  • LinkedHashMap的key和value的值都可以是null, 如果之前存在null, 则会发生value覆盖

Hashtable概述

  • Hashtable和HashMap一样,底层也是哈希表

  • Hashtable是线程安全的,方法上都有synchronized关键字. 使用较少,因为保证线程安全有其他方式

  • Hashtable初始化容量是11, 默认加载因子是0.75

  • Hashtable的扩容策略: 2倍

  • Hashtable中有一些传统方法,这些方法不属于集合框架:

    • 获取所有 key 的迭代器:通过 hashtable.keys() 方法获取 Enumeration 类型的迭代器 keys,然后使用 while 循环和 hasMoreElements() 方法判断是否还有元素,使用 nextElement() 方法获取元素并打印。
    • 获取所有 value 的迭代器:通过 hashtable.elements() 方法获取 Enumeration 类型的迭代器 elements,同样使用 while 循环和 hasMoreElements() 方法判断是否还有元素,使用 nextElement() 方法获取元素并打印。
  • Enumeration(Hashtable 获取的迭代器的类型)的相关方法

    • boolean hasMoreElements(); 是否含有元素
    • E nextElement(); 获取元素
  • Hashtable和HashMap集合的区别:

    • Hashtable是线程安全的,效率低,key和value不允许是null;
    • HashMap不是线程安全的,效率高,key和value允许是null;

Properties概述

Properties 被称为属性类,通常与 xxx.properties 属性文件一同使用。

特点

  • 继承关系:Properties 的父类是 Hashtable,所以它也是线程安全的。
  • 数据类型:Properties 不支持泛型,其键(key)和值(value)只能是 String 类型。

相关方法:

方法签名方法描述
Object setProperty(String key, String value)功能和 put 方法一样,用于设置键值对。
String getProperty(String key)通过键(key)获取对应的值(value)。
Set<String> propertyNames()获取 Properties 中所有的键(key),并以 Set<String> 形式返回。

二叉树概述

二叉树(BinaryTree)由一个结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。
下图中展现了五种不同基本形态的二叉树。

(a) 为空树。
(b) 为仅有一个结点的二叉树。
© 是仅有左子树而右子树为空的二叉树。
(d) 是仅有右子树而左子树为空的二叉树。
(e) 是左、右子树均非空的二叉树。

排序二叉树

  • 排序二叉树采用左小右大原则存储,按照中序遍历方式,自动就是排好序的

    • 中序遍历:左根右
    • 前序遍历:根左右
    • 后序遍历:左右根
  • 比如:我们要将数据【14, 12, 23, 4, 16, 13, 8, 3】存储到排序二叉树中,如右图所示

  • 排序二叉树的问题:排序二叉树本身实现了排序功能,可以快速检索。但如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成普通的链表,其检索效率就会很差。

  • 先进行排序变成:【3, 4, 8, 12, 13, 14, 16, 23】,然后存储到排序二叉树中,显然就变成了链表,如下图所示

平衡二叉树(AVL)

  • 为了避免出现上述一边倒的存储,科学家提出了“平衡二叉树”。

  • 在平衡二叉树中任何结点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除结点可能需要通过一次或多次树旋转来重新平衡这个树。

  • 结点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子-2或2的节点被认为是不平衡的,并需要重新平衡这个树。

  • 比如,我们存储排好序的数据【3, 4, 8, 12, 13, 14, 16, 23】,增加结点如果出现不平衡,则通过节点的左旋或右旋,重新平衡树结构,最终平衡二叉树如下图所示(另参见:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html)

ser-images%5Cimage-20250321122455989.png&pos_id=img-xRXh0sYr-1754143788827)

红黑二叉树

  • 红黑二叉树简称红黑树,他首先是一颗二叉树,同时也是一颗自平衡的排序二叉树
  • 红黑树在原有的排序二叉树增加了如下几点要求
    • 每个结点要么是红色,要么是黑色
    • 根结点永远是黑色
    • 所有的叶子结点都是空结点(即null),是黑色的.
    • 每个红色结点的两个子结点都是黑色的(从每个叶子结点到根结点的路径上不会有两个连续的红色结点).
    • 从任意结点到其子树中每个叶子结点的路径都包含相同数量的黑色结点.
    • 每次新结点在插入时,颜色是红的. 插入后,会根据红黑树的约束条件进行: 树的旋转和颜色的调整
  • 这些约束强化了红黑树的关键性质: 从根到叶子的最长的可能路径不超过最短可能路径的两倍长,这样就让树大致上是平衡的
  • 红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。红黑树的基本操作:插入、删除、左旋、右旋、着色。每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 “左旋、右旋、着色” 操作,使树继续保持红黑树的特性。

TreeMap

  • TreeMap底层就是红黑树
  • TreeMap和HashMap用法一样, 只不过需要key排序的时候就可以使用TreeMap
  • TreeMap的key不能是null,但是value可以是null;
  • 让TreeMap集合可排序有两种方式:
    • 第一种方式: key实现了Comparable接口,并且重写了compareTo方法,在该方法中添加了比较规则(比较规则不变的时候建议使用这种)
    • 第二种方式: 创建TreeMap集合时,提供一个比较器,比较器实现了Comparator接口,在compare方法中添加比较规则

总结哪些集合不能添加null

  • Hashtable的key和value都不能是null
  • properties的key和value都不能是null
  • TreeMap的key不能是null(因为key要进行排序),但value可以是null
  • TreeSet不能添加null

HashSet面试题

HashSet面试题:(前提: Student类中 hashCode()和 equals()方法均已重写)
HashSet set = new HashSet<>();
Student stu = new Student(“张三”, 18);
set.add(stu);
set.add(new Student(“李四”, 21));
stu.setName(“王五”);
// 问题1:请问是否删除了HashSet集合中的stu对象呢???
set.remove(stu);
// 问题2:添加以下Student对象是否成功???
set.add(new Student(“王五”, 18));
// 问题3:添加以下Student对象是否成功???
set.add(new Student(“张三”, 18));

remove()方法执行同样是根据stu的哈希值来计算stu的索引位置,然后使用equals方法找到与stu值相同的结点进行删除

Collections工具类

以下都是静态方法,直接通过类名.来调用

  • 排序方法: Collectons.sort()
  • 打乱顺序:Collections.shuffle()
  • 反转: Collections.reverse()
  • 替换所有元素: Collections.fill()

第八章 IO流

IO流概述

  • 什么是IO流?

    • 水分子的移动形成了水流
    • IO流指的是: 程序中数据的流动. 数据可以从内存流动到硬盘,也可以从硬盘流动到内存.
    • Java中IO流的基本作用是: 完成文件的读和写
  • IO流的分类?

    • 根据数据流向分为:
      • 输入流: 从硬盘流动到内存(输入又叫做读: read)
      • 输出流: 从内存流动到硬盘(输出又叫做写: write)
    • 根据读写数据的形式分为:
      • 字符流: 一次读取一个字符. 只适合读取普通文本. 不适合读取二进制文件. 因为字符流统一使用Unicode编码,可以有效避免出现乱码问题
      • 字节流: 一次读取一个字节. 适合读取非文本数据. 如图片,声音,视频等文件. (当然字节流是万能的, 什么都可以读和写)

    注意Java中所有的IO流中凡是以Stream结尾的都是字节流,以reader和writer结尾的都是字符流

    • 根据流在IO操作中的作用和实现方式来分类:
      • 节点流: 节点流负责数据源和数据目的地的连接,是IO中最基本的数据成分
      • 处理流: 处理流对节点流进行了装饰/包装,提供更多高级处理操作,方便用户进行数据处理
  • Java中已经将IO流实现了,在Java.io包下,可以直接使用

  • 之前学的Collection在Java.util包下

IO流的体系结构

  • 四大头领:
    • InputStream
    • OutputStream
    • Reader
    • Writer
  • File相关的:
    • FileInputStream
    • FileOutputStream
    • FileReader
    • FileWriter
  • 缓冲流相关的:
    • BufferedInputStream
    • BufferedOutputStream
    • BufferedReader
    • BufferedWriter
  • 转换流相关的:
    • InputStreamReader
    • OutputStreamWriter
  • 打印流相关的:
    • PrintStream
    • PrintWriter
  • 对象相关的:
    • ObjectInputStream
    • ObjectOutputStream
  • 数据相关的:
    • DataInputStream
    • DataOutputStream
  • 字节数组相关的
    • ByteArrayInputStream
    • ByteArrayOutputStream
  • 压缩和解压缩相关的:
    • GZIPInputStream
    • GZIPOutputStream
  • 线程相关的:
    • PipedInputStream
    • PipedOutputStream

所有的流都实现了Closeable接口,都有close()方法,流用完要关闭。

所有的输出流都实现了Flushable接口,都有flush()方法,flush方法的作用是,将缓存清空,全部写出。养成好习惯,以防数据丢失。

FileInputeStream

FileInputStream 概述

文件字节输入流,可用于读取任何类型的文件。

常用构造方法

  • FileInputStream(String name):创建一个文件字节输入流对象,参数为文件的路径。

常用方法

  • int read():从文件中读取一个字节(8 个二进制位),返回读取到的字节本身;若未读取到任何数据,则返回 -1。
  • int read(byte[] b):一次读取多个字节。若文件内容充足,一次最多读取 b.length 个字节,返回读取到的字节总数;若未读取到任何数据,则返回 -1。将读取到的数据存入byte数组中,输出数组可以使用String的构造方法将byte数组转换成字符串
  • int read(byte[] b, int off, int len):读取数据后存入 byte 数组时,从 off 位置开始存放,最多读取 len 个字节。若未读取到任何数据,则返回 -1。
  • long skip(long n):跳过 n 个字节。
  • int available():返回流中剩余的估计字节数量。
  • void close():关闭流。

注意事项

当使用 FileInputStream 读取包含中文的文件时,可能会读取到某个汉字的一半。在将 byte[] 数组转换为 String 时,可能会出现乱码问题。因此,FileInputStream 不太适合用于读取纯文本文件。

package com.powernode.javase.io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 *  1.称作文件字节输入流,负责读
 *  2. 是一个万能流,任何文件都能读.但还是建议读二进制文件,例如: 图片,声音,视频等
 *  3. 但是FileInputStream肯定也是可以读普通文本的,只不过一次读取一个字节,容易出现乱码问题
 *  4. FileInputStreanm的常用构造方法:
 *      FileInputStream(String name) 通过文件路径构建一个文件字节输入流对象
 *  5. FileInputStream的常用方法:
 *      int read(); 调用一次read方法则返回一个字节,返回读到的字节本身, 如果读不到任何数据则返回-1
 *      int read(byte[] b); 一次最多可以读到b.length个字节(只要文件中内容足够多),
 *          返回值是读取到的字节数量. 如果没有读到任何数据,返回-1
 *      int read(byte[] b, int off, int length); 一次读取length个字节,将读取到的数据从byte数组的off位置开始放
 *      void close(); 关闭流
 *      long skip(long n); 跳过多少个字节
 *      int available(); 获取流中剩余的估计字节数
 */
public class FileInputStreamTest01 {
    public static void main(String[] args) {
        InputStream in = null;

        try {
            in = new FileInputStream("D:\\编程\\code\\file01.txt");

            int read = 0;
            while ((read = in.read()) != -1){
                System.out.println(read);
            }
            /*判断条件中执行了一次in.read,但是循环体中又一次执行了in.read,所以循环一次相当于2个字节
            while (in.read() != -1){
                System.out.println(in.read());
            }*/
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            if (in != null){
                try {
                        in.close();
                } catch (IOException e) {
                   e.printStackTrace();
                }
            }
        }
    }
}

package com.powernode.javase.io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * 测试 int read(byte[] b); 一次最多可以读到b.length个字节(只要文件中内容足够多),
 * 返回值是读取到的字节数量. 如果没有读到任何数据,返回-1
 */
public class FileInputStreamTest02 {
    public static void main(String[] args) {
        FileInputStream in = null;
        try {
            in = new FileInputStream("D:\\编程\\code\\file01.txt");

            //创建一个byte数组,用于存放读取的数据
            byte[] bytes = new byte[4];

            try {
                /*//第一次读取
                int read = in.read(bytes);
                System.out.println(read);

                //将byte数组转换成字符串
                String s1 = new String(bytes);
                System.out.println(s1);

                //第二次读取
                read = in.read(bytes);
                System.out.println(read);

                String s2 = new String(bytes,0,read);
                System.out.println(s2);

                //第三次读取
                read = in.read(bytes,0,read);
                System.out.println(read);*/
                /*while(true){
                    int readCount = in.read(bytes);
                    if (readCount == -1) break;
                    String s = new String(bytes,0,readCount);
                    System.out.println(s);
                }*/
                int readCount = 0;
                while((readCount = in.read(bytes)) != -1){
                    String s = new String(bytes,0,readCount);
                    System.out.println(s);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally{
            if (in != null){
                try {
                    in.close();
                } catch (IOException e) {
                   e.printStackTrace();
                }
            }
        }
    }
}

package com.powernode.javase.io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 *      int read(byte[] b, int off, int length); 一次读取length个字节,将读取到的数据从byte数组的off位置开始放
 *      void close(); 关闭流
 *      long skip(long n); 跳过多少个字节
 *      int available(); 获取流中剩余的估计字节数
 */
public class FileInputStreamTest03 {
    public static void main(String[] args) {
        InputStream in = null;
        try {
            in = new FileInputStream("D:\\编程\\code\\file01.txt");

            byte[] bytes = new byte[10];
            /*int readCount = in.read(bytes, 2, 5);
            System.out.println("读取到的数据个数是: " + readCount);//5

            for (byte i : bytes){
                System.out.println(i);
            }*/
            int read = in.read();
            System.out.println(read);//97

            in.skip(2);

            read = in.read();
            System.out.println(read);//100

            System.out.println(in.available());

        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            if (in != null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

FileOutputStream

FileOutputStream 概述

文件字节输出流,用于向文件中写入字节数据。

常用构造方法

  • FileOutputStream(String name):创建输出流,会先将指定文件内容清空,然后再不断写入新的数据。
  • FileOutputStream(String name, boolean append):创建输出流,若 append 参数为 true,则会在原文件的末尾以追加的形式不断写入数据。

常用方法

  • void write(int b):向文件中写入一个字节。
  • void write(byte[] b):将字节数组中的所有数据全部写入文件。
  • void write(byte[] b, int off, int len):将字节数组中从 off 位置开始的 len 个字节写入文件。
  • void close():关闭流,释放相关系统资源。
  • void flush():刷新流,确保缓冲区中的数据被写入文件。
package com.powernode.javase.io;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

/**
 * java.io.FileOutputStream
 *      文件字节输出流
 *      常用的构造方法:
 *          FileOutputStream(String name); 创建输出流,文件先清空,再将文件写入
 *          FileOutputStream(String name, boolean append);  创建输出流,在源文件末尾以追加的形式写入
 *      常用的方法:
 *          write(int b); 写入一个字节
 *          void write(byte[] bytes); 将字节数组中所有数据全部写出
 *          void write(byte[] bytes, int off, int len); 将字节数组中的一部分写出
 *          void close(); 关闭流
 *          void flush(); 刷新
 */
public class FileOutputStreamTest01 {
    public static void main(String[] args) {
        OutputStream out = null;
        try {
            out = new FileOutputStream("D:\\编程\\code\\file02.txt");
/*
            out.write(97);//清除文件原先的内容,并写入a
            out.write(98);
            out.write(99);
            out.write(100);*/

            byte[] bytes = {97,98,99,100,101};
            out.write(bytes);//abcde
            out.write(bytes,0,2);//abcdeab

            byte[] bytes1 = "库库学Java".getBytes(StandardCharsets.UTF_8);
            out.write(bytes1);
            //输出流记得刷新
            out.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally{
            //使用结束关闭流
            if (out != null){
                try {
                    out.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

使用 FileInputStreamFileOutputStream 完成文件的复制

package com.powernode.javase.io;

import java.io.*;

public class FileInputOutputStreamCopy {
    public static void main(String[] args) {
        //创建输入流
        InputStream in = null;
        //创建输出流
        OutputStream out = null;

        try {
            in = new FileInputStream("D:\\编程\\code\\动力节点第一章java代码\\HelloWorld.java");
            out = new FileOutputStream("D:\\HelloWorld.java");

            //需要执行的代码
            //一次至少拷贝1kb
            //先使用in.read()将文件中的数据按照1kb的大小读入到数组中
            //在使用out.write()将数组中的数据按照读取的容量写入新的文件中
            byte[] bytes = new byte[1024];
            int readCount = 0;
            while((readCount = in.read(bytes)) != -1){
                out.write(bytes,0,readCount);
            }

            //刷新
            out.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally{
            //关闭流 建议分开try...catch.. 防止in.close()发生错误导致out.close()不能执行
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

资源的自动关闭(try-with-resources)

凡是实现了AutoCloseable接口的流都可以使用try-with-resources, 都可以自动关闭

try-with-resources语法格式:

​ try(

​ 声明流;

​ 声明流;

​ 声明流;

​ 声明流

​ ){

​ }catch(Exception e){

​ }

FileReader - 文件字符输入流

常用的构造方法
  • FileReader(String fileName):通过指定文件名创建一个新的 FileReader 对象。
常用的方法
  • int read():读取单个字符。返回读取的字符,如果已到达流的末尾,则返回 -1。
  • int read(char[] cbuf):将字符读入数组。返回读取的字符数,如果已到达流的末尾,则返回 -1。
  • int read(char[] cbuf, int off, int len):将字符读入数组的一部分。off 是目标数组 cbuf 中的起始偏移量,len 是读取的最大字符数。返回读取的字符数,如果已到达流的末尾,则返回 -1。
  • long skip(long n):跳过 n 个字符。返回实际跳过的字符数。
  • void close():关闭该流并释放与之关联的所有系统资源。

package com.powernode.javase.io;

/*
    FileReader
    	文件字符输入流
    常用的构造方法:
    	FileReader(String fileName)
    常用的方法:
        int read()
        int read(char[] cbuf);
        int read(char[] cbuf, int off, int len);
        long skip(long n);
        void close()
*/

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest01 {
    public static void main(String[] args) {
        try(FileReader reader = new FileReader("D:\\编程\\code\\File01.txt")) {

            /*int read = 0;
            while((read = reader.read()) != -1){
                System.out.print(read);
            }*/
            char[] chars = new char[512];
            int readCount = 0;
            while((readCount = reader.read(chars)) != -1){
                String s = new String(chars,0,readCount);
                System.out.println(s);
            }

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

FileWriter - 文件字符输出流

常用构造方法
  • FileWriter(String fileName):根据指定的文件名创建一个新的FileWriter对象。
  • FileWriter(String fileName, boolean append):根据指定的文件名创建一个FileWriter对象,可通过append参数指定是否以追加模式写入文件。若appendtrue,则在文件末尾追加内容;若为false,则覆盖原有内容。
常用方法
  • void write(char[] cbuf):将字符数组cbuf中的所有字符写入文件。
  • void write(char[] cbuf, int off, int len):从字符数组cbufoff位置开始,将长度为len的字符写入文件。
  • void write(String str):将字符串str写入文件。
  • void write(String str, int off, int len):从字符串stroff位置开始,将长度为len的子字符串写入文件。
  • void flush():刷新输出流,确保所有缓冲的输出字节被写入文件。
  • void close():关闭输出流,释放相关资源。关闭前会自动调用flush()方法。
  • Writer append(CharSequence csq, int start, int end):将字符序列csqstartend的部分追加到文件中。
使用 FileReader 和 FileWriter 拷贝普通文本文件
package com.powernode.javase.io;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class FileReaderFileWriterCopy {
    public static void main(String[] args) {
        try(FileReader reader = new FileReader("D:\\编程\\code\\file03.txt");
            FileWriter writer = new FileWriter("D:\\file03.txt")){

            //定义char数组作为容器
            char[] chars = new char[1024];
            int readCount = 0;
            while((readCount = reader.read(chars)) != -1){
                writer.write(chars,0,readCount);
            }

            //刷新
            writer.flush();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

关于文件的路径问题

} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}


### 资源的自动关闭(try-with-resources)

凡是实现了AutoCloseable接口的流都可以使用try-with-resources, 都可以自动关闭

try-with-resources语法格式:

​	try(

​		声明流;

​		声明流;

​		声明流;

​		声明流

​	){

​	}catch(Exception e){

​	}

## FileReader - 文件字符输入流

#### 常用的构造方法

- **`FileReader(String fileName)`**:通过指定文件名创建一个新的 `FileReader` 对象。

#### 常用的方法

- **`int read()`**:读取单个字符。返回读取的字符,如果已到达流的末尾,则返回 -1。
- **`int read(char[] cbuf)`**:将字符读入数组。返回读取的字符数,如果已到达流的末尾,则返回 -1。
- **`int read(char[] cbuf, int off, int len)`**:将字符读入数组的一部分。`off` 是目标数组 `cbuf` 中的起始偏移量,`len` 是读取的最大字符数。返回读取的字符数,如果已到达流的末尾,则返回 -1。
- **`long skip(long n)`**:跳过 `n` 个字符。返回实际跳过的字符数。
- **`void close()`**:关闭该流并释放与之关联的所有系统资源。

```java

package com.powernode.javase.io;

/*
    FileReader
    	文件字符输入流
    常用的构造方法:
    	FileReader(String fileName)
    常用的方法:
        int read()
        int read(char[] cbuf);
        int read(char[] cbuf, int off, int len);
        long skip(long n);
        void close()
*/

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest01 {
    public static void main(String[] args) {
        try(FileReader reader = new FileReader("D:\\编程\\code\\File01.txt")) {

            /*int read = 0;
            while((read = reader.read()) != -1){
                System.out.print(read);
            }*/
            char[] chars = new char[512];
            int readCount = 0;
            while((readCount = reader.read(chars)) != -1){
                String s = new String(chars,0,readCount);
                System.out.println(s);
            }

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

FileWriter - 文件字符输出流

常用构造方法
  • FileWriter(String fileName):根据指定的文件名创建一个新的FileWriter对象。
  • FileWriter(String fileName, boolean append):根据指定的文件名创建一个FileWriter对象,可通过append参数指定是否以追加模式写入文件。若appendtrue,则在文件末尾追加内容;若为false,则覆盖原有内容。
常用方法
  • void write(char[] cbuf):将字符数组cbuf中的所有字符写入文件。
  • void write(char[] cbuf, int off, int len):从字符数组cbufoff位置开始,将长度为len的字符写入文件。
  • void write(String str):将字符串str写入文件。
  • void write(String str, int off, int len):从字符串stroff位置开始,将长度为len的子字符串写入文件。
  • void flush():刷新输出流,确保所有缓冲的输出字节被写入文件。
  • void close():关闭输出流,释放相关资源。关闭前会自动调用flush()方法。
  • Writer append(CharSequence csq, int start, int end):将字符序列csqstartend的部分追加到文件中。
使用 FileReader 和 FileWriter 拷贝普通文本文件
package com.powernode.javase.io;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class FileReaderFileWriterCopy {
    public static void main(String[] args) {
        try(FileReader reader = new FileReader("D:\\编程\\code\\file03.txt");
            FileWriter writer = new FileWriter("D:\\file03.txt")){

            //定义char数组作为容器
            char[] chars = new char[1024];
            int readCount = 0;
            while((readCount = reader.read(chars)) != -1){
                writer.write(chars,0,readCount);
            }

            //刷新
            writer.flush();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

关于文件的路径问题

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐