28 minute read


1. Java 基础语法

1.1 类型转换

1.1.1 隐式转换

概念

也叫自动类型提升。

​ 就是把一个取值范围小的数据或者变量,赋值给另一个取值范围大的变量。此时不需要我们额外写代码单独实现,是程序自动帮我们完成的。

​ 简单来说:就是小的给大的,可以直接给。

提升规则

  • 取值范围小的,和取值范围大的进行运算,小的会先提升为大的,再进行运算。
  • byte、short、char三种类型的数据在运算的时候,都会直接先提升为int,然后再进行运算。
  • 取值范围从小到大的关系: byte short int long float double

1.1.2 强制转换

概念

​ 如果要把一个取值范围大的数据或者变量赋值给另一个取值范围小的变量。是不允许直接操作。

​ 如果一定要这么干,就需要加入强制转换。

书写格式

​ 目标数据类型 变量名 = (目标数据类型)被强转的数据;

1.2 自增自减运算符

  • 放在变量的前面,我们叫做先++。 比如:++a
  • 放在变量的后面,我们叫做后++。 比如:a++

​ 不管是先++,还是后++。单独写在一行的时候,运算结果是一模一样的。

//自增自减运算符
int num3 = 1;
int num4 = num3++;//自增自减运算符放在后面是 先用后加
System.out.println("num3= " + num3 + '\t' + "num4= " + num4 + '\t' + "num3= " + num3);
int num5 = 1;
int num6 = --num5;//自增自减运算符放在前面是 先计算之后再用
System.out.println("num5= " + num5 + '\t' + "num6= " + num6 + '\t' + "num5= " + num5);

输出为:

num3= 2	num4= 1	num3= 2
num5= 0	num6= 0	num5= 0

1.3 循环例题

/**
 * 给你一个整数x,
 * 如果x时一个回文数,打印true,否则返回false
 * 回文数时指正序(从左到右)和倒序(从右到左)读都是一样的数
 * */
System.out.println("=====================问题二:回文数=======================");
Scanner sc = new Scanner(System.in);
int numOri = sc.nextInt();

//方法一:笨办法,判断对称的两位是否相等 但是会有问题.遇到11位的数时就会报错,这是sc的nextInt的限制
int num = numOri;
//判断位数
int digitCount = 1;
int temp = 10;
while(true){
    if(num / temp == 0){
        System.out.println("输入的这个整数是 " + digitCount + " 位数");
        break;
    }
    else{
        temp *= 10;
        digitCount++;
    }
}
temp /= 10;

for(int i = digitCount; i > digitCount/2; ++i){
    if(num / temp != num % 10) {//最高位和最低为不相等
        System.out.println(false);
        break;
    }
    else{
        num %= temp;//去掉了最高位
        num /= 10;//去掉了最低位
        digitCount -= 2;
        temp /= 100;
        if(temp == 0){ System.out.println(true); break;}
    }
}

//方法二:将数字倒过来和原来的数字进行比较
int num2 = numOri;
int newNum = 0;
while(num2 > 0){
    newNum = newNum * 10 + num2 % 10;//获取最低位,个位,然后进行相加
    num2 /= 10;//去掉最低位
}
System.out.println(newNum);
if(newNum == numOri) System.out.println(true);
else System.out.println(false);

/**
 * 给定两个整数,都是正数且不超过int的范围
 * 将两数相除,要求不使用乘法、除法、和 % 运算符*/
System.out.println("=====================问题三:求商和余数=======================");
System.out.println("请输入被除数:");
int question3_num1 = sc.nextInt();
System.out.println("请输入除数:");
int question3_num2 = sc.nextInt();
int question3_div = 0;
int question3_mod = 0;

if(question3_num2 > question3_num1) {
    question3_div = 0;
    question3_mod = question3_num2;
}
else{
    //思路一:多少个除数相加大于了被除数,根据这个来终止
    int num2_temp = question3_num2;
    int cnt_temp = 0;
    while(num2_temp <= question3_num1){
        num2_temp += question3_num2;
        cnt_temp++;
    }
//            question3_div = cnt_temp;
//            question3_mod = question3_num1 - (num2_temp - question3_num2);

    //思路二:被除数减去多少个除数为负数则终止
    int num1_temp = question3_num1;
    int cnt_temp2 = 0;
    while(num1_temp - question3_num2 >= 0){
        num1_temp -= question3_num2;
        cnt_temp2++;
    }
    question3_div = cnt_temp2;
    question3_mod = num1_temp;
}

System.out.println(question3_num1 + " / " + question3_num2 + " = " + question3_div  + " ········· " + question3_mod);
{
    /**
     * 键盘录入一个大于等于2的整数x,计算并返回x的算术平方根
     * 结果只保留整数部分,小数部分将被舍去*/
    System.out.println("=============问题一:求平方根=====================");
    Scanner input = new Scanner(System.in);
    System.out.println("请输入一个大于等于2的整数 x :");
    int num1 = input.nextInt();
    int start = 1;
    while (start * start <= num1) {
        ++start;
    }
    System.out.println("x 的算术平方根的整数部分是:" + (start - 1));
}

{
    /**
     *键盘录入一个正整数x,判断该数是否为一个质数
        * 质数,只能被1和本身整除的数称为质数
        * */
    System.out.println('\n' + "=============问题二:判断质数=====================");
    System.out.println("请输入一个正整数x:");
    Scanner input2 = new Scanner(System.in);
    int num2 = input2.nextInt();
    if (num2 < 3) System.out.println("这个数是质数");
    else {
        int cnt2 = 0;
        for (int i = 1; i <= num2; ++i) {
            if (num2 % i == 0) ++cnt2;
            if (cnt2 > 2) break;
        }
        if (cnt2 > 2) System.out.println("这个数不是是质数");
        else System.out.println("这个数是质数");
    }
    //思路二,一个数如果不是质数,则 x = a * b,这里必定有 a或者b中有一个小于等于 x开根,另一个大于等于
    //使用这个思路来简化循环次数,只需要循环x的开根这么多次即可
}

{
    /**
     * 电脑随机生成一个数,使用程序来猜这个数字*/
    System.out.println('\n' + "=============问题三:猜数=====================");
    Random rand = new Random();
    Scanner input = new Scanner(System.in);
    int randNum = rand.nextInt(100) + 1;//这里是想要的1到100的范围,那就是100个数'
    int cnt3 = 1;
    while (true) {
        int myNum = input.nextInt();
        if(myNum == randNum){
            System.out.println("猜出来了:" + myNum);
            break;
        }
        else if(myNum < randNum) System.out.println("猜小了");
        else System.out.println("猜大了");
        ++cnt3;
    }
    System.out.println("猜了 " + cnt3 + "次");
}

1.4 数组

初始化

  • 静态初始化:数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3,元素4…};

    或者 简化格式:数据类型[] 数组名 = {元素1,元素2,元素3,元素4…};

    手动指定数组的元素,系统会根据元素的个数,计算出数组的长度。

  • 动态初始化 格式:数据类型[] 数组名 = new 数据类型[数组的长度];

    动态初始化:手动指定数组长度,由系统给出默认初始化值。

1.5 方法重载

  • 方法重载概念

    方法重载指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载

    • 多个方法在同一个类中
    • 多个方法具有相同的方法名
    • 多个方法的参数不相同,类型不同或者数量不同
  • 注意:

    • 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式
    • 重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,换句话说不能通过返回值来判定两个方法是否相互构成重载

1.6 综合案例

package basic;

import java.util.Random;
import java.util.Scanner;

public class Day250907_Demo1 {
    public static void main(String[] args){
        /**
         * 练习一:找出101到200之间有多少个质数,并输出所有质数*/
        System.out.println("=================练习一:找质数====================");
        int cnt1 = 0;
        for(int i = 101; i < 201; i++){
            if(isPrime(i)){
                ++cnt1;
                System.out.print(i + "  ");
                if(cnt1 % 10 == 0)  System.out.println();
            }
        }
        System.out.println("有 " + cnt1 + "个 质数");

        /**
         * 练习二:定义方法实现随机产生一个5位的验证码,
         * 要求验证码格式:
         * 长度为5、前四位是大写字母或者小写字母、最后一位是数字*/
        System.out.println("=================练习二:生成随机验证码====================");
        System.out.println(generateCaptcha());

        /**
         * 练习三:某系统的数字密码(大于0) 采用加密方式进行传输
         * 规则为:先得到每位数,然后每位数都加5,再对10求余,最后将所有数字反转,得到一串新数
         * */
        System.out.println("=================练习三:数字加密====================");
        encryptNum();

        /**
         * 练习四:解密数字*/
        System.out.println("=================练习四:数字解密====================");
        decryptNum();

        /**
         * 练习五:抽奖,奖品奖金分别有{2,588,888,1000,10000}五个奖金,
         * 请使用代码模拟抽奖,打印出每个奖项,奖项的出现顺序随机并且不重复,*/
        System.out.println("=================练习五:随机不重复抽奖====================");
        lottery();

    }
    //练习一: 判断一个数是否为质数 , 这里的循环是循环到该数的一半,其实根据之前说的,
    // 可以只判断到该数的平方根
    public static boolean isPrime(int n){
        if(n < 4) return true;
        else{
            if(n % 2 == 0) return false;
            for(int i = 3; i < n/2; i = i + 2){
                if(n % i == 0) return false;
            }
            return true;
        }
    }

    //练习二:生成随机验证码
    public static String generateCaptcha(){
        String letter = new String();
        //for(int i = 48; i <= 57; ++i) letter = letter + (char)i;//数字  数字可以不用字符类型,加号会自动进行转化
        for(int i = 65; i <= 90; ++i) letter = letter + (char)i;//大写字母
        for(int i = 97; i <= 122; ++i) letter = letter + (char)i;//小写字母

        Random rand = new Random();
        int randNum1 = rand.nextInt(52);
        int randNum2 = rand.nextInt(52);
        int randNum3 = rand.nextInt(52);
        int randNum4 = rand.nextInt(52);
        int randNum5 = rand.nextInt(10);
        return "" + letter.charAt(randNum1) + letter.charAt(randNum2) + letter.charAt(randNum3)
                + letter.charAt(randNum4) + randNum5;
    }

    //练习三:数字加密
    public static int encryptNum(){
        Scanner input = new Scanner(System.in);
        System.out.println("请输入要加密的数字:");
        int oriNum = input.nextInt();
        int digCount = getDigitCount(oriNum);
        System.out.println("数字的位数:" + digCount + '\n' + "倒序的数字为");
        int [] everyNums = getEveryNum(oriNum);
        for(int i = 0; i < digCount; ++i){
            System.out.print(everyNums[i] + "  ");
        }
        System.out.println('\n' + "处理后:");
        for(int i = 0; i < digCount; ++i){
            everyNums[i] += 5;
            everyNums[i] %= 10;
            System.out.print(everyNums[i] + "  ");
        }

        int result = 0;
        for(int i = 0; i < digCount; ++i){
            result = result * 10 + everyNums[i];
        }
        System.out.println('\n' + "最终加密后的数字为:" + result);
        return result;
    }
    public static int getDigitCount(int num){
        int digitCount = 0;
        int count = 1;
        while(num / count != 0){
            ++digitCount;
            count *= 10;
        }
        return digitCount;
    }
    public static int [] getEveryNum(int num){
        int [] result = new int [getDigitCount(num)];
        for(int i = 0; i < result.length; ++i){
            result[i] = num % 10;
            num = num/10;  //这个数组中存的顺序是 低位 是数字的 低位
        }
        return result;
    }

    //练习四:解密数字
    public static int decryptNum(){
        Scanner input = new Scanner(System.in);
        System.out.println("请输入要解密的数字:");
        int oriNum = input.nextInt();
        int digCount = getDigitCount(oriNum);
        int [] everyNums = getEveryNum(oriNum);

        for(int i = 0; i < digCount; ++i){
            if(everyNums[i] >= 5) everyNums[i] -= 5;
            else everyNums[i] += 5;
        }

        int result = 0;
        for(int i = 0; i < digCount; ++i){
            result = result * 10 + everyNums[i];
        }
        System.out.println('\n' + "最终解密后的数字为:" + result);
        return result;
    }

    //练习五:随机抽奖不重复
    public static void lottery(){
        Random rand = new Random();
        int [] price = {2, 588, 888, 1000, 10000};

        System.out.println("思路一:新建一个获奖数组进行比较");
        int [] newArr = new int [price.length];
        for(int i = 0; i < newArr.length; ){
            int randNum = rand.nextInt(price.length);
            if(isExist(newArr, price[randNum])) System.out.println("抱歉本次抽奖未中");
            else{
                newArr[i] = price[randNum];
                System.out.println(price[randNum] + "元的奖金被抽出");
                ++i;
            }

        }

        System.out.println("思路二:修改奖池数组");
        while(price[0] + price[1] + price[2]
            + price[3] + price[4] !=0){
            int randNum = rand.nextInt(price.length);
            if(price[randNum] != 0) {
                System.out.println(price[randNum] + "元的奖金被抽出");
                price[randNum] = 0;
            }
            else System.out.println("抱歉本次抽奖未中");
        }


    }
    public static boolean isExist(int [] arr, int num){
        for(int i = 0; i < arr.length; ++i){
            if(arr[i] == num) return true;
        }
        return false;
    }
}
package basic;

import java.util.Scanner;
import java.util.Random;

public class Day250908_Demo2 {
    public static void main(String[] args){
        /**
         *  练习六:双色球系统:
         *  投注号码由6个红色球号码和1个蓝色球号码组成,红色球号码从1-33中选择;蓝色球号码从1-16中选择
         *  双色球中奖条件和奖金表:
         *                  中奖条件            奖金
         *  一等奖:6个红球号码+1个蓝球号码        最高1000万
         *  二等奖:6个红球号码+0个蓝球号码        最高500万
         *  三等奖:5个红球号码+1个蓝球号码        3000元
         *
         *  四等奖:5个红球号码+0个蓝球号码
         *        或4个红球号码+1个蓝球号码        200元
         *
         *  五等奖:红4+蓝0                       10元
         *         或3+1
         *
         *  六等奖:红2+蓝1                       5元
         *        或红1+蓝1
         *        或红0+蓝1
         * */
        //大致步骤:随机生成中奖号码,用户输入自己号码,判断中奖情况
        Scanner input = new Scanner(System.in);
        int[] your_num = new int[7];
        System.out.println("请输入你的号码(前六位是红色球号码,第七位是蓝色球号码)");
        for(int i = 0; i < your_num.length - 1;){
            int randNum = input.nextInt();
            if(!isExist(your_num, your_num.length - 1, randNum)){
                your_num[i] = randNum;
                ++i;
            }
            else System.out.println("输入号码重复了,请重新输入");
        }
        your_num[6] = input.nextInt();
        System.out.println("你的号码:");
        for(int i = 0; i < your_num.length; ++i){System.out.print(your_num[i] + "   ");}
        System.out.println("中奖号码:");
        int [] prizeNum = generatePrize();
        for(int i = 0; i < prizeNum.length; ++i){System.out.print(prizeNum[i] + "   ");}
        isWinner(your_num, prizeNum);
//        mySort(your_num, your_num.length - 1);
//        System.out.println();
//        for(int i = 0; i < your_num.length; ++i){System.out.print(your_num[i] + "   ");}
    }
    public static boolean isExist(int [] arr, int length, int num){
        for(int i = 0; i < length; ++i){
            if(arr[i] == num) return true;
        }
        return false;
    }
    public static int[] generatePrize(){//前6个是红球获奖号码,最后一个是篮球获奖号码
        Random rand = new Random();
        int [] price = new int [7];
        for(int i = 0; i < 6; ) {
            int randNum = rand.nextInt(33) + 1;
            if(!isExist(price, price.length - 1, randNum)){
                price[i] = randNum;
                ++i;
            }
        }
        price[6] = rand.nextInt(16) + 1;
        return price;
    }
    public static int howManyEqualInRed(int [] your_num, int [] prize_num){
        boolean [] flag = new boolean [6];
        int cnt = 0;
        for(int i = 0; i < your_num.length - 1; ++i){
            for(int j = 0; j < your_num.length - 1; ++j){
                if(your_num[j] == prize_num[j] && !flag[j] ) {
                    flag[j] = true;
                    ++cnt;
                    break;
                }
            }
        }
        return cnt;
    }
//    public static boolean isEqual(int [] arr1, int [] arr2){
//        if(arr1.length != arr2.length) return false;
//        mySort(arr1, arr1.length - 1);
//        mySort(arr2, arr2.length - 1); //只排序红色球
//
//        for(int i = 0; i < arr1.length; ++i){
//            if(arr1[i] != arr2[i]) return false;
//        }
//        return true;
//    }
//    public static void mySort(int [] arr, int num){//从小到大排序arr数组中前num个数
//        if(arr.length < 2) return;
//        if(arr.length < num) num = arr.length;
//        for(int i = 0; i < num -1; ++i){
//            for(int j = 0; j < num - i - 1; ++j){
//                if(arr[j] > arr[j + 1]) {
//                    int temp = arr[j];
//                    arr[j] = arr[j + 1];
//                    arr[j + 1] = temp;
//                }
//            }
//        }
//    }

    public static void isWinner(int [] your_num, int [] prizeNum){
        int cntRedEqual = howManyEqualInRed(your_num, prizeNum);
        if(cntRedEqual == 6){
            if(your_num[your_num.length - 1] == prizeNum[prizeNum.length - 1])
                System.out.println("恭喜你获得了一等奖,最高奖金1000万");
            else System.out.println("恭喜你获得了二等奖,最高奖金500万");
        }
        else if(cntRedEqual == 5){
            if(your_num[your_num.length - 1] == prizeNum[prizeNum.length - 1])
                System.out.println("恭喜你获得了三等奖,奖金3000元");
            else System.out.println("恭喜你获得了四等奖,奖金200元");
        }
        else if(cntRedEqual == 4){
            if(your_num[your_num.length - 1] == prizeNum[prizeNum.length - 1])
                System.out.println("恭喜你获得了四等奖,奖金3000元");
            else System.out.println("恭喜你获得了五等奖,奖金10元");
        }
        else if(cntRedEqual == 3 && your_num[your_num.length - 1] == prizeNum[prizeNum.length - 1])
            System.out.println("恭喜你获得了五等奖,奖金10元");
        else if(your_num[your_num.length - 1] == prizeNum[prizeNum.length - 1] && (cntRedEqual ==2 || cntRedEqual ==1 || cntRedEqual ==0))
            System.out.println("恭喜你获得了六等奖,奖金5元");
        else
            System.out.println("每中奖");
    }

}

1.7 类和对象

客观存在的事物皆为对象 ,所以我们也常常说万物皆对象。

    • 类的理解
      • 类是对现实生活中一类具有共同属性和行为的事物的抽象
      • 类是对象的数据类型,类是具有相同属性和行为的一组对象的集合
      • 简单理解:类就是对现实事物的一种描述
    • 类的组成
      • 属性:指事物的特征,例如:手机事物(品牌,价格,尺寸)
      • 行为:指事物能执行的操作,例如:手机事物(打电话,发短信)
  • 类和对象的关系
    • 类:类是对现实生活中一类具有共同属性和行为的事物的抽象
    • 对象:是能够看得到摸的着的真实存在的实体
    • 简单理解:类是对事物的一种描述,对象则为具体存在的事物

1.8 String类

  • 字符串不可变,它们的值在创建后不能被更改
  • 虽然 String 的值是不可变的,但是它们可以被共享
  • 字符串效果上相当于字符数组( char[] ),但是底层原理是字节数组( byte[] )

1.8.1 String类的构造方法

  • 常用的构造方法

    方法名 说明
    public String() 创建一个空白字符串对象,不含有任何内容
    public String(char[] chs) 根据字符数组的内容,来创建字符串对象
    public String(byte[] bys) 根据字节数组的内容,来创建字符串对象
    String s = “abc”; 直接赋值的方式创建字符串对象,内容就是abc

1.8.2 创建字符串对象两种方式的区别

  • 通过构造方法创建

    ​ 通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同

  • 直接赋值方式创建

    ​ 以“”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会建立一个 String 对象,并在字符串池中维护

1.8.3 字符串的比较

直接赋值构造的字符串 String 会存储到 内存中的串池中,即当使用双引号进行直接赋值时,系统会检查该字符串在串池中是否存在,如果不存在,则创建新的,存在则进行复用。

手动 new 出来的会在堆中开辟一个新的空间,不会复用,就不会复用内存

==号的作用

  • 比较基本数据类型:比较的是具体的值
  • 比较引用数据类型:比较的是对象地址值

如果比较的是基本数据类型,int double byte short等,比的就是具体数据的大小

如果是String这类引用数据类型进行比较,比较的就是地址值,所以如果是使用引号进行构造时,可以使用 == 号来比较(因为在串池中地址相同),而使用new出的字符串

然后键盘录入得到的字符串也是new出来的

```java
int [] arr1 = {1,2};
int [] arr2 = {1,2};
String s6 = new String("aaa");
String s7 = new String("aaa");
String s8 = "aaa";
String s9 = "aaa";
System.out.println("arr1 的地址 是: " + arr1);
System.out.println("arr2 的地址 是: " + arr2);
System.out.println("s6 的地址 是: " + s6);
System.out.println("s7 的地址 是: " + s7);
System.out.println("s8 的地址 是: " + s8);
System.out.println("s9 的地址 是: " + s9);

System.out.println("arr1 == arr2 的比较结果 " + (arr1 == arr2));
System.out.println("s6 == s7 的比较结果 " + (s6 == s7));
System.out.println("s8 == s9 的比较结果 " + (s8 == s9));
```

最终的输出结果是:

```bash
==============练习二: 等号的问题==============
arr1 的地址 是: [I@b4c966a
arr2 的地址 是: [I@1d81eb93
s6 的地址 是: aaa
s7 的地址 是: aaa
s8 的地址 是: aaa
s9 的地址 是: aaa
arr1 == arr2 的比较结果 false
s6 == s7 的比较结果 false
s8 == s9 的比较结果 true
```

equals方法的作用

  • 方法介绍

    public boolean equals(String s)     比较两个字符串内容是否相同区分大小写
    

    s6.equals(要比较的字符串)的输出来进行比较,不忽略大小写 s6.equalsIgnoreCase(s10) 忽略大小写

      String s11 = "abc";
      String s12 = "a" + "b" + "c";
      System.out.println("s11 ==  s12 的结果:"+ (s11 ==  s12));
      String s13 = "ab";
      String s14 = s13 + "c";
      System.out.println("s11 ==  s14 的结果:"+ (s11 ==  s14));
    

    第一个中s12是直接相加没有变量的参与,然后编译器在编译时会自动进行优化,因此,在编译时就会将 "a" + "b" + "c" 拼接为 "abc",会复用串池中的字符串,然后又都是串池中的字符串,因此地址一样,输出是true

    而第二个中s14 则有变量的参与每一行都会创建一个新的字符串,地址不一样,输出结果是false

  • 截取对应的字符串 res += num.substring(0, 3);

  • 替代字符串中的 某些元素 str.replace("23", "AAAAA")

1.9 String 相关的容器

StringBuilder

  • StringBuilder 可以看作一个容器,创建之后里面的内容是可变的

    例如 在String类型进行拼接时,拼接时就会生成一个新的字符串,然后将新的字符串的地址赋值给原来的变量,如果拼接次数多了,会影响运行速度,这种情况下采用StringBuilder会提高效率, StringBuilder 的构造函数,空参构造,有参构造(以一个String类型作为参数) 方法:

    • StringBuilder append(任意类型) 添加数据,并返回对象本身
    • StringBuilder reverse() 反转容器中的内容
    • int length() 返回长度
    • String toString() 将StringBuilder 转换为String
      String str = "";
      long start = System.nanoTime();
      for(int i = 0; i < 10000; ++i) str += "a";
      long end = System.nanoTime();
      System.out.println("String直接拼接运行时间" + (end-start));
    
      StringBuilder str2 = new StringBuilder();
      long start2 = System.nanoTime();
      for(int i = 0; i < 10000; ++i) str2.append("a");
      long end2 = System.nanoTime();
      System.out.println("StringBuilder 拼接运行时间" + (end2-start2));
    

    输出为

      String直接拼接运行时间12280000
      StringBuilder 拼接运行时间367000
    

    StringBuilder的效率高的原因,默认创建一个长度为16的字节数组,添加的内容长度小于16,则直接存入,添加的内容大于16则会扩容(原来的容量*2+2),若添加的内容超过了扩容后的容量就直接以添加的内容的长度作为实际容量

StringJoiner

使用时需要导入库 import java.util.StringJoiner;

  • 构造函数

    public StringJoiner(间隔符号) 用来指定拼接时的间隔符号

    public StringJoiner(间隔符号,开始符号,结束符号) 用来指定拼接时的间隔符号,开始符号和结束符号

  • 一些方法

      StringJoiner sj = new StringJoiner("-----", "[", "]");
      //添加元素
      sj.add("1").add("2").add("3").add("4").add("5").add("6").add("7");
      System.out.println(sj);
      //返回长度
      System.out.println(sj.length());
      //转化为字符串
      String str = sj.toString();
      System.out.println(str);
    

1.10 ArrayList类

构造方法

方法名 说明
public ArrayList() 创建一个空的集合对象

成员方法

方法名 说明
public boolean add(要添加的元素) 将指定的元素追加到此集合的末尾
public boolean remove(要删除的元素) 删除指定元素,返回值表示是否删除成功
public E remove(int index) 删除指定索引处的元素,返回被删除的元素
public E set(int index,E element) 修改指定索引处的元素,返回被修改的元素
public E get(int index) 返回指定索引处的元素
public int size() 返回集合中的元素的个数

1.11 static关键字

1.当 static 修饰成员变量或者成员方法时,该变量称为静态变量,该方法称为静态方法。该类的每个对象都共享同一个类的静态变量和静态方法。任何对象都可以更改该静态变量的值或者访问静态方法。但是不推荐这种方式去访问。因为静态变量或者静态方法直接通过类名访问即可,完全没有必要用对象去访问。

2.无static修饰的成员变量或者成员方法,称为实例变量,实例方法,实例变量和实例方法必须创建类的对象,然后通过对象来访问。

3.static修饰的成员属于类,会存储在静态区,是随着类的加载而加载的,且只加载一次,所以只有一份,节省内存。存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。它优先于对象存在,所以,可以被所有对象共享。

4.无static修饰的成员,是属于对象,对象有多少个,他们就会出现多少份。所以必须由对象调用。

1.12 继承

需要注意:Java是单继承的,一个类只能继承一个直接父类

并不是父类的所有内容都可以给子类继承的:

子类不能继承父类的构造方法。

值得注意的是子类可以继承父类的私有成员(成员变量,方法),只是子类无法直接访问而已,可以通过getter/setter方法访问父类的private成员变量。

在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造调用时,一定先调用父类的构造方法。

  • 子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。

  • super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

  • super(..)和this(…)是根据参数去确定调用父类哪个构造方法的。
  • super(..)可以调用父类构造方法初始化继承自父类的成员变量的数据。
  • this(..)可以调用本类中的其他构造方法。

1.13 多态

多态是继封装、继承之后,面向对象的第三大特性。

多态是出现在继承或者实现关系中的

多态体现的格式

父类类型 变量名 = new 子类/实现类构造器;
变量名.方法名();

多态的前提:有继承关系,子类对象是可以赋值给父类类型的变量。例如Animal是一个动物类型,而Cat是一个猫类型。Cat继承了Animal,Cat对象也是Animal类型,自然可以赋值给父类类型的变量。

  1. 有继承或者实现关系

  2. 方法的重写【意义体现:不重写,无意义】

  3. 父类引用指向子类对象【格式体现】

    父类类型:指子类对象继承的父类类型,或者实现的父接口类型。

用途、使用场景: 如果一个注册方法,传递的参数只能选择一种,如果传递学生,就无法传递老师和管理员,这时利用多态就可以将方法的形参定义为它们共同的父类Person

  • 当一个方法的形参是一个类,我们可以传递这个类所有的子类对象。
  • 当一个方法的形参是一个接口,我们可以传递这个接口所有的实现类对象(后面会学)。
  • 而且多态还可以根据传递的不同对象来调用不同类中的方法。

多态的运行特点

调用成员变量时:编译看左边,运行看左边

调用成员方法时:编译看左边,运行看右边

代码示例:

Fu f = new Zi()
//编译看左边的父类中有没有name这个属性,没有就报错
//在实际运行的时候,把父类name属性的值打印出来
System.out.println(f.name);
//编译看左边的父类中有没有show这个方法,没有就报错
//在实际运行的时候,运行的是子类中的show方法
f.show();
/**
* 多态相关的,变量和方法,不太一样,
* 成员变量的话,编译看左边,运行看左边
* 编译看左边,编译代码时,会看左边的父类中有没有这个变量,如果有,编译成功,反之失败
* 运行看左边:运行代码时,实际获取的就是左边父类中成员变量的值
*
* 成员方法,编译看左边,运行看右边
* 编译看左边同上,
* 运行看右边,实际获取的子类中的成员方法
*/

多态的弊端

我们已经知道多态编译阶段是看左边父类类型的,如果子类有些独有的功能,此时多态的写法就无法访问子类独有功能了

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做向下转型。

回顾基本数据类型转换

  • 自动转换: 范围小的赋值给范围大的.自动完成:double d = 5;
  • 强制转换: 范围大的赋值给范围小的,强制转换:int i = (int)3.14

​ 多态的转型分为向上转型(自动转换)与向下转型(强制转换)两种。

向上转型(自动转换)

  • 向上转型:多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。 当父类引用指向一个子类对象时,便是向上转型。 使用格式:
父类类型  变量名 = new 子类类型();
Animal a = new Cat();

原因是:父类类型相对与子类来说是大范围的类型,Animal是动物类,是父类类型。Cat是猫类,是子类类型。Animal类型的范围当然很大,包含一切动物。所以子类范围小可以直接自动转型给父类类型的变量。

向下转型(强制转换)

  • 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。 一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

使用格式:

子类类型 变量名 = (子类类型) 父类变量名;
如:Aniaml a = new Cat();
   Cat c =(Cat) a;  

为了保证类型转换时没有子类之间相互转换的情况,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:

变量名 instanceof 数据类型 
如果变量属于该数据类型或者其子类类型返回true
如果变量不属于该数据类型或者其子类类型返回false

所以,转换前,我们最好先做一个判断,代码如下:

public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();               // 调用的是 Cat 的 eat

        // 向下转型  
        if (a instanceof Cat){
            Cat c = (Cat)a;       
            c.catchMouse();        // 调用的是 Cat 的 catchMouse
        } else if (a instanceof Dog){
            Dog d = (Dog)a;       
            d.watchHouse();       // 调用的是 Dog 的 watchHouse
        }
    }  
}

instanceof新特性

JDK14的时候提出了新特性,把判断和强转合并成了一行

//新特性
//先判断a是否为Dog类型,如果是,则强转成Dog类型,转换之后变量名为d
//如果不是,则不强转,结果直接是false
if(a instanceof Dog d){
    d.lookHome();
}else if(a instanceof Cat c){
    c.catchMouse();
}else{
    System.out.println("没有这个类型,无法转换");
}

1.14 权限修饰符

​ 在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,我们之前已经学习过了public 和 private,接下来我们研究一下protected和默认修饰符的作用。

  • public:公共的,所有地方都可以访问。

  • protected:本类 ,本包,其他包中的子类都可以访问。

  • 默认(没有修饰符):本类 ,本包可以访问。

    注意:默认是空着不写,不是default

  • private:私有的,当前类可以访问。 public > protected > 默认 > private

不同权限的访问能力

  public protected 默认 private
同一类中
同一包中的类  
不同包的子类    
不同包中的无关类      

可见,public具有最大权限。private则是最小权限。

编写代码时,如果没有特殊的考虑,建议这样使用权限:

  • 成员变量使用private ,隐藏细节。
  • 构造方法使用 public ,方便创建对象。
  • 成员方法使用public ,方便调用方法。

小贴士:不加权限修饰符,就是默认权限

1.15 final关键字

​ 学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。

如果有一个方法我不想别人去改写里面内容,该怎么办呢?

Java提供了final 关键字,表示修饰的内容不可变。

  • final: 不可改变,最终的含义。可以用于修饰类、方法和变量。
    • 类:被修饰的类,不能被继承。
    • 方法:被修饰的方法,不能被重写。
    • 变量:被修饰的变量,有且仅能被赋值一次。

查询API发现像 public final class Stringpublic final class Mathpublic final class Scanner 等,很多我们学习过的类,都是被final修饰的,目的就是供我们使用,而不让我们所以改变其内容。

1.16 抽象类

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定),父类只需要提供一个没有方法体的定义即可,具体实现交给子类自己去实现。我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类

  • 抽象方法 : 没有方法体的方法。
  • 抽象类:包含抽象方法的类。

abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。

使用abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:

修饰符 abstract 返回值类型 方法名 (参数列表)

抽象类的使用要求:继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

    理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类,编译无法通过而报错。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

  5. 抽象类存在的意义是为了被子类继承。

    理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。

抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义。抽象类可以强制让子类,一定要按照规定的格式进行重写。

1.17 接口

我们已经学完了抽象类,抽象类中可以用抽象方法,也可以有普通方法,构造方法,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,JDK7之前,包括JDK7,接口中全部是抽象方法。接口同样是不能创建对象的

定义格式

//接口的定义格式:
interface 接口名称{
    // 抽象方法
}

// 接口的声明:interface
// 接口名称:首字母大写,满足“驼峰模式”

抽象方法

注意:接口中的抽象方法默认会自动加上public abstract修饰程序员无需自己手写!!
​       
按照规范:以后接口中的抽象方法建议不要写上public abstract。

常量

在接口中定义的成员变量默认会加上: public static final修饰。也就是说在接口中定义的成员变量实际上是一个常量。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量。常量必须要给初始值。常量命名规范建议字母全部大写,多个单词用下划线连接。

基本的实现

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 ` implements`关键字。

/**接口的实现:
    在Java中接口是被实现的,实现接口的类称为实现类。
    实现类的格式:*/
class 类名 implements 接口1,接口2,接口3...{

}

从上面格式可以看出,接口是可以被多实现的。

类实现接口的要求和意义

  1. 必须重写实现的全部接口中所有抽象方法。
  2. 如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成抽象类。
  3. 意义:接口体现的是一种规范,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。

Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口。大家一定要注意:

类与接口是实现关系

接口与接口是继承关系

接口继承接口就是把其他接口的抽象方法与本接口进行了合并。

关于接口的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 当两个接口中存在相同抽象方法的时候,该怎么办?

只要重写一次即可。此时重写的方法,既表示重写1接口的,也表示重写2接口的。

  1. 实现类能不能继承A类的时候,同时实现其他接口呢?

继承的父类,就好比是亲爸爸一样 实现的接口,就好比是干爹一样 可以继承一个类的同时,再实现多个接口,只不过,要把接口里面所有的抽象方法,全部实现。

  1. 实现类能不能继承一个抽象类的时候,同时实现其他接口呢?

实现类可以继承一个抽象类的同时,再实现其他多个接口,只不过要把里面所有的抽象方法全部重写。

  1. 实现类Zi,实现了一个接口,还继承了一个Fu类。假设在接口中有一个方法,父类中也有一个相同的方法。子类如何操作呢?

处理办法一:如果父类中的方法体,能满足当前业务的需求,在子类中可以不用重写。 处理办法二:如果父类中的方法体,不能满足当前业务的需求,需要在子类中重写。

  1. 如果一个接口中,有10个抽象方法,但是我在实现类中,只需要用其中一个,该怎么办?

可以在接口跟实现类中间,新建一个中间类(适配器类) 让这个适配器类去实现接口,对接口里面的所有的方法做空重写。 让子类继承这个适配器类,想要用到哪个方法,就重写哪个方法。 因为中间类没有什么实际的意义,所以一般会把中间类定义为抽象的,不让外界创建对象

1.18 内部类

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。可以把内部类理解成寄生,外部类理解成宿主。

什么时候使用内部类:一个事物内部还有一个独立的事物,内部的事物脱离外部的事物无法独立使用

  1. 人里面有一颗心脏。
  2. 汽车内部有一个发动机。
  3. 为了实现更好的封装性。

内部类的分类 按定义的位置来分

  1. 成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)
  2. 静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)
  3. 局部内部类,类定义在方法内
  4. 匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。

1.18.1 成员内部类

成员内部类特点

  • 无static修饰的内部类,属于外部类对象的。
  • 宿主:外部类对象。

内部类的使用格式

 外部类.内部类 // 访问内部类的类型都是用 外部类.内部类

获取成员内部类对象的两种方式

方式一:外部直接创建成员内部类的对象

外部类.内部类 变量 = new 外部类().new 内部类();

方式二:在外部类中定义一个方法提供内部类的对象

案例演示

方式一
public class Test {
    public static void main(String[] args) {
        //  宿主:外部类对象。
       // Outer out = new Outer();
        // 创建内部类对象。
        Outer.Inner oi = new Outer().new Inner();
        oi.method();
    }
}

class Outer {
    // 成员内部类,属于外部类对象的。
    // 拓展:成员内部类不能定义静态成员。
    public class Inner{
        // 这里面的东西与类是完全一样的。
        public void method(){
            System.out.println("内部类中的方法被调用了");
        }
    }
}


方式二
public class Outer {
    String name;
    private class Inner{
        static int a = 10;
    }
    public Inner getInstance(){
        return new Inner();
    }
}

public class Test {
    public static void main(String[] args) {
        Outer o = new Outer();
        System.out.println(o.getInstance());


    }
}

成员内部类的细节

编写成员内部类的注意点:

  1. 成员内部类可以被一些修饰符所修饰,比如: private,默认,protected,public,static等
  2. 在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。
  3. 创建内部类对象时,对象中有一个隐含的Outer.this记录外部类对象的地址值。(请参见3.6节的内存图)

详解:

​ 内部类被private修饰,外界无法直接获取内部类的对象,只能通过之前的方式二(返回一个内部类对象的方法)获取内部类的对象

​ 被其他权限修饰符修饰的内部类一般用之前的方式一(外部类.内部类创建对象然后进行获取) 直接获取内部类的对象

​ 内部类被static修饰是成员内部类中的特殊情况,叫做静态内部类下面单独学习。

​ 内部类如果想要访问外部类的成员变量,外部类的变量必须用final修饰,JDK8以前必须手动写final,JDK8之后不需要手动写,JDK默认加上。

成员内部类例题

请在?地方填上相应代码,以达到输出的内容

注意:内部类访问外部类对象的格式是:外部类名.this

public class Test {
    public static void main(String[] args) {
        Outer.inner oi = new Outer().new inner();
        oi.method();
    }
}

class Outer {	// 外部类
    private int a = 30;

    // 在成员位置定义一个类
    class inner {
        private int a = 20;

        public void method() {
            int a = 10;
            System.out.println(???);	// 10   答案:a
            System.out.println(???);	// 20	答案:this.a
            System.out.println(???);	// 30	答案:Outer.this.a
        }
    }
}

1.18.2 静态内部类

静态内部类特点

  • 静态内部类是一种特殊的成员内部类。

  • 有static修饰,属于外部类本身的。
  • 总结:静态内部类与其他类的用法完全一样。只是访问的时候需要加上外部类.内部类。
  • 拓展1:静态内部类可以直接访问外部类的静态成员。
  • 拓展2:静态内部类不可以直接访问外部类的非静态成员,如果要访问需要创建外部类的对象。
  • 拓展3:静态内部类中没有银行的Outer.this。

内部类的使用格式

外部类.内部类。

静态内部类对象的创建格式

外部类.内部类  变量 = new  外部类.内部类构造器;

调用方法的格式:

  • 调用非静态方法的格式:先创建对象,用对象调用
  • 调用静态方法的格式:外部类名.内部类名.方法名();

案例演示

// 外部类:Outer01
class Outer01{
    private static  String sc_name = "黑马程序";
    // 内部类: Inner01
    public static class Inner01{
        // 这里面的东西与类是完全一样的。
        private String name;
        public Inner01(String name) {
            this.name = name;
        }
        public void showName(){
            System.out.println(this.name);
            // 拓展:静态内部类可以直接访问外部类的静态成员。
            System.out.println(sc_name);
        }
    }
}

public class InnerClassDemo01 {
    public static void main(String[] args) {
        // 创建静态内部类对象。
        // 外部类.内部类  变量 = new  外部类.内部类构造器;
        Outer01.Inner01 in  = new Outer01.Inner01("张三");
        in.showName();
    }
}

1.18.3 局部内部类

  • 局部内部类 :定义在方法中的类。

定义格式:

class 外部类名 {
	数据类型 变量名;
	
	修饰符 返回值类型 方法名(参数列表) {
		// …
		class 内部类 {
			// 成员变量
			// 成员方法
		}
	}
}

1.18.4 匿名内部类【重点】

是内部类的简化写法。他是一个隐含了名字的内部类。开发中,最常用到的内部类就是匿名内部类了。

格式

new 类名或者接口名() {
     重写方法;
};

包含了:

  • 继承或者实现关系

  • 方法重写
  • 创建对象

所以从语法上来讲,这个整体其实是匿名内部类对象

/**
 * 正常的类如果有继承父类或者实现接口的话是:
 * public class 子类或者实现类名 extends 父类 implement 接口名(){
 *
 * }
 * 现在想实现匿名的话,就省略前面的 class 类名,还想要创建这个没有名字的类的对象,只能使用之前的new格式
 * 
 */



/**
 * 需求:
 * 一个方法参数是一个父类,调用的是继承这个父类的子类,然后,如果不采用匿名对象类的话,
 * 就需要自己创建一个子类的对象,传递给方法,但是如果这个子类只使用一次的话,就略繁琐,
 * 那么就采用匿名内部类
 *
 * 使用场景,当方法的参数或者类时,以接口为例,可以传递这个接口的实现类对象,如果实现类只使用一次
 * 就可以用匿名内部类简化*/

匿名内部类前提和格式

匿名内部类必须继承一个父类或者实现一个父接口

匿名内部类格式

new 父类名或者接口名(){
    // 方法重写
    @Override 
    public void method() {
        // 执行语句
    }
};

以接口为例,匿名内部类的使用,代码如下:

interface Swim {
    public abstract void swimming();
}

public class Demo07 {
    public static void main(String[] args) {
        // 使用匿名内部类
		new Swim() {
			@Override
			public void swimming() {
				System.out.println("自由泳...");
			}
		}.swimming();

        // 接口 变量 = new 实现类(); // 多态,走子类的重写方法
        Swim s2 = new Swim() {
            @Override
            public void swimming() {
                System.out.println("蛙泳...");
            }
        };

        s2.swimming();
        s2.swimming();
    }
}

匿名内部类的特点

  1. 定义一个没有名字的内部类
  2. 这个类实现了父类,或者父类接口
  3. 匿名内部类会创建这个没有名字的类的对象

匿名内部类的使用场景

通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:

interface Swim {
    public abstract void swimming();
}

public class Demo07 {
    public static void main(String[] args) {
        // 普通方式传入对象
        // 创建实现类对象
        Student s = new Student();
        
        goSwimming(s);
        // 匿名内部类使用场景:作为方法参数传递
        Swim s3 = new Swim() {
            @Override
            public void swimming() {
                System.out.println("蝶泳...");
            }
        };
        // 传入匿名内部类
        goSwimming(s3);

        // 完美方案: 一步到位
        goSwimming(new Swim() {
            public void swimming() {
                System.out.println("大学生, 蛙泳...");
            }
        });

        goSwimming(new Swim() {
            public void swimming() {
                System.out.println("小学生, 自由泳...");
            }
        });
    }

    // 定义一个方法,模拟请一些人去游泳
    public static void goSwimming(Swim s) {
        s.swimming();
    }
}

1.19 类和接口的区别

  1. 语法层面

    • 类:只能 单继承(Java 为了避免菱形继承问题)。
    • 接口:可以 多实现,解决了“类的多继承”需求。
  2. 设计意图

    • 抽象类/类 → 事物的本质和属性(“是什么”,is-a)。

      比如:Animal → 有名字、有年龄,会吃饭,会叫。

    • 接口 → 行为能力的规范(“能做什么”,can-do)。

      比如:Flyable → 能飞,Swimmable → 能游泳。

  3. 团队协作上的作用

    • 抽象类:为一类事物提供 部分默认实现,让子类减少重复代码。
    • 接口:提供 统一的调用规范,让不同人写的类可以协同工作。

对比点 抽象类 接口
继承关系 只能单继承 可以多实现
成员 可以有变量、构造方法、抽象方法、普通方法 成员变量只能是常量(public static final),方法默认是抽象的(Java 8+ 有 default/static)
设计目的 定义“是什么” 定义“能做什么”
复用性 可以提供部分默认实现 不能复用代码,主要是规范
典型场景 一类事物的共性(模板) 不同类的通用行为(能力)

1.20 lambda 表达式

package Day250918_algorithm;

import java.util.Arrays;
import java.util.Comparator;

public class Day250919_lambdaTest {
    public static void main(String[] args){
        /**
         * lambda 表达式格式:(形参)->{方法体}
         * 可以用来简化匿名内部类的书写
         * lambda 表达式只能简化函数式接口的匿名内部类的写法
         * 有且只有一个 抽象方法的接口叫做函数式接口  在接口上可以加  @FunctionalInterface 这个注解来进行验证*/

        /**
         * 参数类型可以省略不写,
         * 如果只有一个参数,参数类型可以省略,同时()也可以省略
         * 如果lambda 表达式的方法体只有一行,大括号,分号,return 可以省略不写,需要通过省略*/
        //test1();
        test2();

    }
    public static void test1(){
        Integer[] arr1 = {4,8,66,2,1,0};
        Arrays.sort(arr1, new Comparator<Integer>() {
            @Override
            // o1 参数;表示在无序序列中,遍历得到的每个元素
            // o2 参数:有序序列中的元素
            public int compare(Integer o1, Integer o2) {
                return (o2 - o1);
                //负数说明 有序序列中的数小于无序序列中的数,放前面,说明是降序
            }
        });
        System.out.println(Arrays.toString(arr1));
        //lambda 表达式:删掉外层的 new 接口名 和对应的大括号 ,删掉函数名之前的,只保留参数和函数体
        Integer[] arr2 = {4,8,66,2,1,0};
        Arrays.sort(arr2, (Integer o1, Integer o2)-> {
                return (o2 - o1);
            }
        );
        System.out.println(Arrays.toString(arr2));
    }

    public static void test2(){
        method(new Swim(){
            @Override
            public void swim(){
                System.out.println("重写了swim方法");
            }
        });
        method(()->{
            System.out.println("lambda重写");
        });
        method(()->
            System.out.println("省略lambda重写"));
    }
    public static void method(Swim s){
        System.out.println("调用method方法");
        s.swim();
    }



}

@FunctionalInterface
interface Swim{
    public abstract void swim();
}

2. 常用API

2.1 Math类

Math类所在包为java.lang包,因此在使用的时候不需要进行导包。并且Math类被final修饰了,因此该类是不能被继承的。

Math类包含执行基本数字运算的方法,我们可以使用Math类完成基本的数学运算。Math类中的方法都是静态的,因此在使用的时候我们可以直接通过类名去调用。在Math类中

常见方法

public static int abs(int a)					// 返回参数的绝对值
public static double ceil(double a)				// 返回大于或等于参数的最小整数
public static double floor(double a)			// 返回小于或等于参数的最大整数
public static int round(float a)				// 按照四舍五入返回最接近参数的int类型的值
public static int max(int a,int b)				// 获取两个int值中的较大值
public static int min(int a,int b)				// 获取两个int值中的较小值
public static double pow (double a,double b)	// 计算a的b次幂的值
public static double random()					// 返回一个[0.0,1.0)的随机值

2.2 System类

常见方法

public static long currentTimeMillis()			// 获取当前时间所对应的毫秒值(当前时间为0时区所对应的时间即就是英国格林尼治天文台旧址所在位置)
public static void exit(int status)				// 终止当前正在运行的Java虚拟机,0表示正常退出,非零表示异常退出
public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length); // 进行数值元素copy

arraycopy方法参数说明:

// src: 	 源数组
// srcPos:  源数值的开始位置
// dest:    目标数组
// destPos: 目标数组开始位置
// length:   要复制的元素个数
public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length); 

代码如下所示:

public class SystemDemo01 {

    public static void main(String[] args) {

        // 定义源数组
        int[] srcArray = {23 , 45 , 67 , 89 , 14 , 56 } ;

        // 定义目标数组
        int[] desArray = new int[10] ;

        // 进行数组元素的copy: 把srcArray数组中从0索引开始的3个元素,从desArray数组中的1索引开始复制过去
        System.arraycopy(srcArray , 0 , desArray , 1 , 3);

        // 遍历目标数组
        for(int x = 0 ; x < desArray.length ; x++) {
            if(x != desArray.length - 1) {
                System.out.print(desArray[x] + ", ");
            }else {
                System.out.println(desArray[x]);
            }

        }

    }

}

2.3 Object类

Object类所在包是java.lang包。Object 是类层次结构的根,每个类都可以将 Object 作为超类。所有类都直接或者间接的继承自该类;换句话说,该类所具备的方法,其他所有类都继承了。

2.3.1 常用方法

public String toString()//返回该对象的字符串表示形式(可以看做是对象的内存地址值)
public boolean equals(Object obj)//比较两个对象地址值是否相等;true表示相同,false表示不相同
protected Object clone()   //对象克隆
/**object 中只有空参构造*/
/**
 * public String toString() 返回对象的字符串表示形式 默认是返回地址,如果想返回里面的内容,
 * 可以进行重写 System 的print函数也 依靠 于 这个重写后的toString函数
 * public boolean equals(Object obj) 比较两个对象是否相等 同样比较的也是地址值
 * protected Object clone(int a) 对象克隆
 * 对象克隆的步骤
 * 重写object中的clone方法
 * 让你的类实现Cloneable接口,注意自己写的空接口不可以
 * 创建对象并调用clone即可
 *
 * 浅拷贝和深拷贝
 * 浅拷贝:不管对象内部的属性是基本数据类型还是引用数据类型,都完全拷贝过来
 * 深拷贝:基本数据类型进行拷贝,字符串进行复用(串池中的) 引用数据类型会重新创建新的
 * object 中的clone是浅拷贝
 * */

例如:

@Override
public String toString() {
    return name + " " + age + '\t';
}
@Override
public boolean equals(Object obj) {
    if(obj == this) return true;
    if(obj == null || !(obj instanceof Day250917_p2 newObj)) return false;
    //上面这句语句中,在判断类型时就进行了强制类型 转换
    return this.name.equals(newObj.name)
            && this.age == newObj.age;
}
//浅克隆
//    @Override
//    protected Object clone() throws CloneNotSupportedException{
//        /**
//         * 调用父类中的clone方法,
//         * 相当于让java帮我们克隆一个对象,并把克隆后的对象返回出去
//         *
//         *  然后当前类实现一个Cloneable的接口,而这个接口是空的,如果一个接口中没有抽象方法
//         *  表示该接口是一个标记型接口
//         *  现在Cloneable接口表示一旦实现,那么当前类的对象就可以被克隆
//         *  如果没有实现,当前类的对象就不能克隆
//         * */
//        return super.clone();//因为是受保护的权限,因此不能在外部直接调用方法
//    }

//深克隆
@Override
protected Object clone() throws CloneNotSupportedException{
    /**
     * 太繁琐*/
//        Day250917_p3 newP3 = new Day250917_p3();
//        newP3.name = this.name;
//        newP3.age = this.age;
//        newP3.arr = new int[this.arr.length];
//        for (int i = 0; i < this.arr.length; i++) {newP3.arr[i] = this.arr[i];}
//        return newP3;
    int [] newArr = new int[this.arr.length];
    for (int i = 0; i < this.arr.length; i++) {newArr[i] = this.arr[i];}
    Day250917_p3 newRes = (Day250917_p3) super.clone();
    newRes.arr = newArr;
    return newRes;
}

2.4 Objects类

public static String toString(Object o) 					// 获取对象的字符串表现形式
public static boolean equals(Object a, Object b)			// 比较两个对象是否相等
public static boolean isNull(Object obj)					// 判断对象是否为null
public static boolean nonNull(Object obj)					// 判断对象是否不为null

Objects类 是为了解决:

//之前的判断,如果调用者是null时,就会报错,为了解决,要不加一个前置的非空判断,
//或者使用objects

这是Objects类中的源方法:

public static boolean equals(Object a, Object b) {
        return a == b || a != null && a.equals(b);
    }

2.5 包装类

Java提供了两个类型系统,基本类型与引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:

基本类型 对应的包装类(位于java.lang包中)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

2.5.1 Integer类

  • Integer类概述

    包装一个对象中的原始类型 int 的值

    包装类 Integer 在进行加减等基本运算时会进行自动转换为基本数据类型

  • Integer类构造方法及静态方法

方法名 说明
public Integer(int value) 根据 int 值创建 Integer 对象(过时)
public Integer(String s) 根据 String 值创建 Integer 对象(过时)
public static Integer valueOf(int i) 返回表示指定的 int 值的 Integer 实例
public static Integer valueOf(String s) 返回保存指定String值的 Integer 对象
static string tobinarystring(int i) 得到二进制
static string tooctalstring(int i) 得到八进制
static string toHexstring(int i) 得到十六进制
static int parseInt(string s) 将字符串类型的整数转成int类型的整数

3. 算法与数据结构

3.1 查找算法

3.1.1 基本查找

​ 也叫做顺序查找

​ 说明:顺序查找适合于存储结构为数组或者链表。

基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线的一端开始,顺序扫描,依次将遍历到的结点与要查找的值相比较,若相等则表示查找成功;若遍历结束仍没有找到相同的,表示查找失败。

3.1.2 二分查找

​ 也叫做折半查找

说明:元素必须是有序的,从小到大,或者从大到小都是可以的。

如果是无序的,也可以先进行排序。但是排序之后,会改变原有数据的顺序,查找出来元素位置跟原来的元素可能是不一样的,所以排序之后再查找只能判断当前数据是否在容器当中,返回的索引无实际的意义。

  基本思想:也称为是折半查找,属于有序查找算法。用给定值先与中间结点比较。比较完之后有三种情况:

  • 相等

    说明找到了

  • 要查找的数据比中间节点小

    说明要查找的数字在中间节点左边

  • 要查找的数据比中间节点大

    说明要查找的数字在中间节点右边

public static boolean binarySearch(ArrayList<Integer> arr, int target) {
    //二分查找必须是有序的
    Collections.sort(arr);
    int low = 0;
    int high = arr.size() - 1;
    while(low <=  high){
        int mid = (low + high)/2;
        if(arr.get(mid) == target) return true;
        else if(arr.get(mid) < target) low = mid + 1;
        else high = mid - 1;
    }
    return false;
}

3.1.3 插值查找

在二分查找的基础上,让中间的mid点,尽可能靠近想要查找的元素,就能提高查找的效率

二分查找中查找点计算如下:

  mid=(low+high)/2, 即mid=low+1/2*(high-low);

我们可以将查找的点改进为如下:

  mid=low+(key-a[low])/(a[high]-a[low])*(high-low),

这样,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。

  基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。

细节:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。

代码跟二分查找类似,只要修改一下mid的计算方式即可。

斐波那契查找

在介绍斐波那契查找算法之前,我们先介绍一下很它紧密相连并且大家都熟知的一个概念——黄金分割。

  黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。

  0.618被公认为最具有审美意义的比例数字,这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。因此被称为黄金分割。

  在数学中有一个非常有名的数学规律:斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….

(从第三个数开始,后边每一个数都是前两个数的和)。

然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。

基本思想:也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。

斐波那契查找也是在二分查找的基础上进行了优化,优化中间点mid的计算方式即可

3.2 排序算法

3.2.1 冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。

它重复的遍历过要排序的数列,一次比较相邻的两个元素,如果他们的顺序错误就把他们交换过来。

public static void bubbleSort(ArrayList<Integer> arr){
    /**
     * n   个数,第1轮比较 n - 1 遍, 会将最大的数放到最后,
     * n-1 个数,第2轮比较 n - 2 遍
     * 。。。
     * n - i + 1 个数 第i轮比较 n - i 遍
     * 。。。
     * 3   个数,第n-2轮,比较 2 遍,
     * 2   个数,第n-1轮,比较 1 遍
     * */
    for(int i = 1; i < arr.size(); ++i){
        for(int j = 1; j < arr.size() - i + 1; ++j){
            if(arr.get(j - 1) > arr.get(j)){
                int temp = arr.get(j - 1);
                arr.set(j - 1, arr.get(j));
                arr.set(j, temp);
            }
        }
    }
    //return arr;
}

3.2.2 选择排序

  1. 从0索引开始,跟后面的元素一一比较
  2. 小的放前面,大的放后面
  3. 第一次循环结束后,最小的数据已经确定
  4. 第二次循环从1索引开始以此类推
  5. 第三轮循环从2索引开始以此类推
  6. 第四轮循环从3索引开始以此类推。
public static void selectionSort(ArrayList<Integer> arr){
    /**
     * n   个 数,第1轮, 第1个数和后面的 n-1 个数进行比较,比较 n - 1遍 ,如果比第一个数小就交换 -- 第0轮,第0个数和后面的比较 n-1 次
     * n-1 个数, 第2轮, 第2个数和后面的 n-2 个数进行比较,比较 n - 2遍,                    -- 第1轮,第1个数和后面的比较 n-2次
     * 。。。                                                                        --
     *           第i轮,                              比较 n-i 遍                     -- 第i-1轮,第i-1个数 比较 n-i
     *。。。                                                                         -- 第i 轮,第i 个数 比较 n-i - 1
        * 3 个数,  第 n -2 轮,                          比较 2 遍                       -- 第n-3轮,   比较2遍
        * 2 个数    第n-1 轮                              比较1遍                        -- 第n-2轮,   比较1遍
        * */
    //下面注释的版本不对,这个和冒泡的思路一样,正确的是后面那段
//        for(int i = 0; i < arr.size() - 1; ++i){
//            for(int j = i + 1; j < arr.size(); ++j){
//                if(arr.get(i) > arr.get(j)){
//                    int temp = arr.get(i);
//                    arr.set(i, arr.get(j));
//                    arr.set(j, temp);
//                }
//            }
//        }
    //这个就是先不进行交换,找到最小的,换到前面
    for(int i = 0; i < arr.size() - 1; ++i){
        int minIndex = i;
        for(int j = i + 1; j < arr.size(); ++j){
            if(arr.get(minIndex) > arr.get(j)){
                minIndex = j;
            }
        }
        int temp = arr.get(i);
        arr.set(i, arr.get(minIndex));
        arr.set(minIndex, temp);
    }
}

3.2.3 插入排序

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过创建有序序列和无序序列,然后再遍历无序序列得到里面每一个数字,把每一个数字插入到有序序列中正确的位置。

插入排序在插入的时候,有优化算法,在遍历有序序列找正确位置时,可以采取二分查找

将0索引的元素到N索引的元素看做是有序的,把N+1索引的元素到最后一个当成是无序的。

遍历无序的数据,将遍历到的元素插入有序序列中适当的位置,如遇到相同数据,插在后面。

N的范围:0~最大索引

public static void insertionSort(ArrayList<Integer> arr){
    /**
     * 第0轮,0索引的元素是有序的,比较0和1 索引的数,比较1次,有序了
     * 第1轮,0 1 的元素是有序的,2索引的数从后向前比较0 1索引的数,比较2次
     * .。。
     * 第i轮,0,1,。。。i的元素是有序的,i+1的元素从后向前比较,比较i次
     * 。。。
     * 第n-3轮,0,1,。。。n-3的元素是有序的,n-2的元素从n-3到0从后向前比较 n-2次
     * n-2 轮   0,1.。。n-2 的元素是有序的, 比较n-1次*/
    //通过 不断交换相邻元素 来完成“插入”,
//        for(int i = 0; i < arr.size() - 1; ++i){
//            for(int j = i; j >= 0; --j){
//                if(arr.get(j + 1) < arr.get(j)){
//                    int temp = arr.get(j + 1);
//                    arr.set(j + 1, arr.get(j));
//                    arr.set(j, temp);
//                }
//            }
//        }

    //先保存 arr[i+1] 的值,然后在前面找插入位置,把大的元素后移,最后一次性插入。
    for(int i = 1; i < arr.size(); ++i){
        int key = arr.get(i);//当前要插入的值
        int j = i - 1;//从 第 i-1 索引来进行向前遍历
        while(j >= 0 && arr.get(j) > key){
            arr.set(j+1, arr.get(j));//元素后移
            j--;
        }
        arr.set(j+1, key);// 插入
    }
}

3.2.4 快速排序

  1. 从数列中挑出一个元素,一般都是左边第一个数字,称为 “基准数”;
  2. 创建两个指针,一个从前往后走,一个从后往前走。
  3. 先执行后面的指针,找出第一个比基准数小的数字
  4. 再执行前面的指针,找出第一个比基准数大的数字
  5. 交换两个指针指向的数字
  6. 直到两个指针相遇
  7. 将基准数跟指针指向位置的数字交换位置,称之为:基准数归位。
  8. 第一轮结束之后,基准数左边的数字都是比基准数小的,基准数右边的数字都是比基准数大的。
  9. 把基准数左边看做一个序列,把基准数右边看做一个序列,按照刚刚的规则递归排序
/**递归就是方法自己调用自己, 递归要有出口,否则会溢出*/
public static void quickSort(ArrayList<Integer> arr, int left, int right){
    /**
     * 第1轮,把0索引的数 作为基准数, 确定基准数在数组中的正确位置,最终实现一个比 基准数小的全部在左边,比基准数大的全部在右边
     * 选基准值 一般选择数组的第一个元素、最后一个元素,或者中间的一个。
     * 分区 遍历数组,把小于基准的放左边,大于基准的放右边,最后基准值放到中间。这样一来,基准值就处于它“最终正确的位置”上了。
     * 递归排序  对左边的子数组递归排序. 对右边的子数组递归排序
     * 终止条件  当子数组的元素数量 ≤ 1 时,不需要排序。*/

    /** 递归出口
     * [2  1]   --  [2  1 ]
     *  b  i/j       b  j   i  然后交换 b 和 j ,这就是后面分区函数中 为什么要和 j 进行交换,而且在while中 要加入 =号
     * */
    if(left >= right) return;//当子数组长度为1或者0时就不需要进行排序了
    int basicIndex = partition(arr, left, right);//得到基准数在子数组中的 位置,并将基准数放到对应位置
    quickSort(arr, left, basicIndex - 1);
    quickSort(arr, basicIndex + 1, right);
}
public static int partition(ArrayList<Integer> arr, int left, int right){
    int basic = arr.get(left);//选择子区间第一个数为基准数
    int i = left + 1;//
    int j = right;

    while(true){
        //从左边找到第一个 大于 基准数 的元素
        while(i <= j && arr.get(i) <= basic){i++;}
        //从右面找到第一个 小于 基准数 的元素
        while(i <= j && arr.get(j) >= basic){j--;}
        //退出条件:
        /**
         *  [3,2,4]--[3,2,4] i>j退出,此时,将b和j位置的数进行交换
         *   b i j -- b j i
         *  [1,5,4]--[1, 5, 4] 继续 [1  5  4]     i > j退出,此时将 b和j进行交换
         *   b i j -- b i/j        b/j  i
         *  [5 5 4] -- [5 5 4]  退出 交换 b 和j
         *   b i j     b    i/j
         * */
        if(i >= j) break;

        //如果没有退出的话, 交换 i和j 位置的数,将大于基准数的元素 放到右边,小于 基准数的元素放到左边
        int temp = arr.get(i);
        arr.set(i, arr.get(j));
        arr.set(j, temp);
    }
    arr.set(left, arr.get(j));
    arr.set(j, basic);
    return j;//返回最终j 的位置,即 基准数的 索引位置
}

3.2.5 系统的排序

通过使用Arrays的sort方法,然后重写比较函数实现从大到小或者从小到大排序:

public static void test1(){

    Day250919_Person p1 = new Day250919_Person("abc", 12, 120);
    Day250919_Person p2 = new Day250919_Person("hdg", 12, 122);
    Day250919_Person p3 = new Day250919_Person("adg", 12, 120);
    Day250919_Person p4 = new Day250919_Person("adg", 15, 120);
    Day250919_Person[] personArr = {p1, p2,p3,p4};

    Arrays.sort(personArr, new Comparator<Day250919_Person>() {
        @Override
        public int compare(Day250919_Person o1, Day250919_Person o2){
            if(o1.getAge() != o2.getAge()) return o1.getAge() - o2.getAge();
                // //负数说明 无序序列中的数小于有序序列中的数,放前面,说明是升序
            else if(o1.getHigh() != o2.getHigh()) return o1.getHigh() - o2.getHigh();
            else {
                //比较字符串,一个一个进行比较:
//                    for (int i = 0; i < Math.min(o1.getName().length(), o2.getName().length()); i++) {
//                        if (o1.getName().charAt(i) != o2.getName().charAt(i)) {
//                            return o1.getName().charAt(i) - o2.getName().charAt(i);
//                        }
//                    }
//                    return o1.getName().length() - o2.getName().length();
                //利用string的方法  就是将o1 的 字符串 减去 o2 的字符串
                return o1.getName().compareTo(o2.getName());
            }
        }
    });
    for(Day250919_Person p : personArr){
        System.out.println(p);
    }
}

3.3 递归练习

/**
     * 有一对兔子,从出生后第三个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一堆兔子,假如兔子都不死,
     * 问第十二个月的兔子有多少对
     * 第一个月         1
     * 第二个月         1
     * 第三个月        1    1
     * 第四个月       1 1   1
     * 第五个月     1 1 1  1 1
     *
     * 这个月的兔子 = 上上个月的兔子成熟了可以再生出同样对数的兔子  +  上个月的兔子总数
     * (上个月兔子包括上上个月兔子(可以生的) 和 剩下的 )
     * F(n) = F(n-1) + F(n-2)*/
    //test2();
    /**
     * 有一堆桃子,猴子第一天吃了其中的一半,并多吃了一个,以后猴子每天都吃当前剩下的一半再多吃一个
     * 第10天时,还没有吃,发现只剩下一个桃子了,请问最初有多少桃子*/
    /**
     * 10   1
     * 9   (1+1)*2 x/2-1 =1 ---- x =  4
     * 8   (4+1)*2
     * */
    //test3();
    /**
     * 小明有时一次爬一个台阶,有时一次爬两个台阶,
     * 如果一个楼梯有20个台阶,小明一共有多少种爬法*/
    /**
     * 1层台阶                    --1种
     * 2        1 2              --2种
     * 3        111、12、 21      --3种
     * 4   1111 121 211 112 22   --5种
     * 5  11111 1112 1121 1211 2111 122 212 221 --8种
     * 也是斐波那契数列
     * 反向思考: 如果小明现在在第19个台阶,向第20个台阶爬,那么只有一种爬法,
     * 所以从第19到第20 的爬法等于 从第1到第19阶的爬法
     * 从第18到第20的爬法看似有两种:直接爬上去,或者一个一个爬,但是
     * 如果一个一个爬,就和从第19到第20爬重复了,
     * 所以  F(20) = F(19) + F(18) */
public static void test2(){
    int month = 12;
    System.out.println(" 有:"+fibo(month));
}
public static int fibo(int n){
    if(n == 1) return 1;
    if(n == 2) return 1;
    return fibo(n - 1) + fibo(n - 2);
}

public static void test3(){
    System.out.println(monkey(1));//求第一天开始的桃子数
}
public static int monkey(int day){
    if(day == 10) return 1;
    return (monkey(day + 1) + 1) * 2;
}

public static void test4(){
    System.out.println("2 种跳法 " + fiboTaijie1(20));
    //改进版,小明一次可以爬1个2个3个台阶,问20个台阶有几种爬法
    System.out.println("3 种跳法 " + fiboTaijie2(20));
}
public static int fiboTaijie1(int n){
    if(n == 1) return 1;
    if(n == 2) return 2;
    return fiboTaijie1(n - 1) + fiboTaijie1(n - 2);
}
public static int fiboTaijie2(int n){
    if(n == 1) return 1;
    if(n == 2) return 2;
    if(n == 3) return 4;//111 12 21 3
    //if(n == 4) return 6;//1111 112 121 211 13 31
    return fiboTaijie2(n - 1) + fiboTaijie2(n - 2) + fiboTaijie2(n - 3);
}

3.3 二叉树

平衡二叉树 左旋: 确定支点:从添加的节点开始,不断的往父节点找不平衡的节点 以不平衡的点作为支点,把支点左旋降级,变成左子节点,晋升原来的右子节点 遇到根节点不平衡, 以不平衡的点作为支点,把根节点的右侧往左拉,原来的右子节点变成新的父节点,并把多余的左子节点让出,给已经降级的根节点当右子节点

右旋: 确定支点:从添加的节点开始,不断的往父节点找不平衡的节点 以不平衡的点作为支点,把支点向右旋降级,变成右子节点,晋升原来的左子节点 遇到根节点不平衡, 以不平衡的点作为支点,把根节点的左侧往右拉,原来的左子节点变成新的父节点,并把多余的右子节点让出,给已经降级的根节点当左子节点

4. 集合 Collection

01_集合类体系结构图

  • Collection集合概述

    • 是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素
    • JDK 不提供此接口的任何直接实现.它提供更具体的子接口(如Set和List)实现
  • 创建Collection集合的对象

/**
 * Collection 是接口,想使用接口的方法有:
 * 一个类实现一个接口,class 类名 implement 接口名{}
 * 或者采用匿名内部类的方式:
 * new 接口名{}
 * 接口多态,Collection<String> c = new ArrayList<>();
 * 左边:Collection<String> → 接口类型(父类型)。
 * 右边:ArrayList、HashSet → 都是 Collection 的实现类(子类型)。
 * */
  • 多态的方式
  • 具体的实现类ArrayList

  • Collection集合常用方法

    方法名 说明
    boolean add(E e) 添加元素
    boolean remove(Object o) 从集合中移除指定的元素
    boolean removeIf(Object o) 根据条件进行移除
    void clear() 清空集合中的元素
    boolean contains(Object o) 判断集合中是否存在指定的元素
    boolean isEmpty() 判断集合是否为空
    int size() 集合的长度,也就是集合中元素的个数

    其中contains方法,经过查看ArrayList的源码得知,是依赖于 equals 方法的,如下,因此,如果在 Collections 中存放自定义数据类型,需要重写 equals 方法

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    return -1;
}

4.1 Collection集合的遍历

4.1.1 迭代器遍历

  • 迭代器介绍

    • 迭代器,集合的专用遍历方式
    • Iterator iterator(): 返回此集合中元素的迭代器,通过集合对象的iterator()方法得到

不依赖索引,在迭代器遍历时,不能使用集合的方法进行增加或者删除,遍历之后可以,遍历时想删除可以使用迭代器的 remove 方法

  • Iterator中的常用方法

    ​ boolean hasNext(): 判断当前位置是否有元素可以被取出 E next(): 获取当前位置的元素,将迭代器对象移向下一个索引位置

  • Collection集合的遍历

    public class IteratorDemo1 {
        public static void main(String[] args) {
            //创建集合对象
            Collection<String> c = new ArrayList<>();
      
            //添加元素
            c.add("hello");
            c.add("world");
            c.add("java");
            c.add("javaee");
      
            //Iterator<E> iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
            Iterator<String> it = c.iterator();
      
            //用while循环改进元素的判断和获取
            while (it.hasNext()) {
                String s = it.next();
                System.out.println(s);
            }
        }
    }
    
  • 迭代器中删除的方法

    ​ void remove(): 删除迭代器对象当前指向的元素

    public class IteratorDemo2 {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            list.add("a");
            list.add("b");
            list.add("b");
            list.add("c");
            list.add("d");
      
            Iterator<String> it = list.iterator();
            while(it.hasNext()){
                String s = it.next();
                if("b".equals(s)){
                    //指向谁,那么此时就删除谁.
                    it.remove();
                }
            }
            System.out.println(list);
        }
    }
    

    4.1.2 增强for

  • 介绍

    • 它是JDK5之后出现的,其内部原理是一个Iterator迭代器
    • 实现Iterable接口的类才可以使用迭代器和增强for
    • 简化数组和Collection集合的遍历
    • 只有单列集合和数组能使用这个方法来进行遍历
  • 格式

    ​ for(集合/数组中元素的数据类型 变量名 : 集合/数组名) {

    ​ // 已经将当前遍历到的元素封装到变量中了,直接使用变量即可

    ​ }

内部本质还是一个迭代器,如果在遍历中修改值不会改变集合中的内容

4.1.3 lambda表达式

​ 利用forEach方法,再结合lambda表达式的方式进行遍历

public class A07_CollectionDemo7 {
    public static void main(String[] args) {
       /* 
        lambda表达式遍历:
                default void forEach(Consumer<? super T> action):
        */

        //1.创建集合并添加元素
        Collection<String> coll = new ArrayList<>();
        coll.add("zhangsan");
        coll.add("lisi");
        coll.add("wangwu");
        //2.利用匿名内部类的形式
        //底层原理:
        //其实也会自己遍历集合,依次得到每一个元素
        //把得到的每一个元素,传递给下面的accept方法
        //s依次表示集合中的每一个数据
       /* coll.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });*/

        //lambda表达式
        coll.forEach(s -> System.out.println(s));
    }
}

或者这样

Collection<String> c1 = new ArrayList<>();
        c1.add("a"); c1.add("b"); c1.add("c"); c1.add("d");
        c1.forEach(new Consumer<String>() {
            @Override
            public void accept(String t) {
                System.out.print(t + "  ");
            }
        });

4.2 List集合

  • List集合的概述
    • 有序集合,这里的有序指的是存取顺序
    • 用户可以精确控制列表中每个元素的插入位置,用户可以通过整数索引访问元素,并搜索列表中的元素
    • 与Set集合不同,列表通常允许重复的元素
  • List集合的特点
    • 存取有序
    • 可以重复
    • 有索引
  • 方法介绍

    方法名 描述
    void add(int index,E element) 在此集合中的指定位置插入指定的元素
    E remove(int index) 删除指定索引处的元素,返回被删除的元素
    E set(int index,E element) 修改指定索引处的元素,返回被修改的元素
    E get(int index) 返回指定索引处的元素
List<Integer> c = new ArrayList<>();
c.add(1); c.add(2); c.add(3);
System.out.println(c);
c.remove(1); // List中的删除,是删除指定索引的内容
System.out.println(c);
c.remove((Integer)1);  //Collection中的删除,是删除指定内容的元素
System.out.println(c);

输出为:

[1, 2, 3]
[1, 3]
[3]

遍历方式

除了 它的 父类中的三种遍历,还可以通过 for 的索引便利,还有列表迭代器遍历

列表迭代器遍历:

List<String> list = new ArrayList<>();
list.add("aaa");list.add("bbb");list.add("ccc");
System.out.println(list);
ListIterator<String> li = list.listIterator();
while(li.hasNext()){
    String s = li.next();
    if(s.equals("aaa")){
        li.add("qqq"); //在这个位置添加对应的内容,然后原来后面的内容会后移
    }
}
System.out.println(list);

输出为:

[aaa, bbb, ccc]
[aaa, qqq, bbb, ccc]

4.3 源码分析

4.3.1 ArrayList源码分析:

核心步骤:

  1. 创建ArrayList对象的时候, 有参构造时:就是按照传入的参数来创建一个对应长度的数组:
     public ArrayList(int initialCapacity) {
         if (initialCapacity > 0) {
             this.elementData = new Object[initialCapacity];
         } else if (initialCapacity == 0) {
             this.elementData = EMPTY_ELEMENTDATA;
         } else {
             throw new IllegalArgumentException("Illegal Capacity: "+
                                                 initialCapacity);
         }
     }
    

    无参构造时:先创建了一个长度为0的数组。

     public ArrayList() {
         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
     }
    
     private static final Object[] EMPTY_ELEMENTDATA = {}; // 是空的数组
    

    数组名字:elementDate,定义变量size。

    size这个变量有两层含义: ①:元素的个数,也就是集合的长度 ②:下一个元素的存入位置

  2. 添加元素,添加完毕后,size++
     public boolean add(E e) {
         modCount++;
         add(e, elementData, size);
         return true;
     }
    
     private void add(E e, Object[] elementData, int s) {
         if (s == elementData.length)// 如果数组满了
             elementData = grow();// 扩容
         elementData[s] = e;// 把元素放到数组末尾
         size = s + 1; // 更新size
     }
     private Object[] grow() {
         return grow(size + 1);
     }
     private Object[] grow(int minCapacity) {//传入的 minCapacity 表示至少需要的容量(一般是 size + 1)。
         int oldCapacity = elementData.length;
         if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
             //如果原数组容量 > 0,或者不是默认的“空数组”,就调用 ArraysSupport.newLength(...) 计算新的容量。
             int newCapacity = ArraysSupport.newLength(oldCapacity,
                     minCapacity - oldCapacity, /* minimum growth */
                     oldCapacity >> 1           /* preferred growth */);
                     //newCapacity = oldCapacity + max(minimumGrowth, preferredGrowth);所以至少需要1.5倍的原来的容量
             return elementData = Arrays.copyOf(elementData, newCapacity);
         } else {
             return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
             //如果原来就是空数组,则分配一个容量为 max(DEFAULT_CAPACITY, minCapacity) 的新数组(默认容量是 10)。
         }
     }
    
     public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
         // preconditions not checked because of inlining
         // assert oldLength >= 0
         // assert minGrowth > 0
    
         int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
         if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
             return prefLength;
         } else {
             // put code cold in a separate method
             return hugeLength(oldLength, minGrowth);
         }
     }
     public boolean addAll(Collection<? extends E> c) {
         // 将传入的集合转换成数组,方便批量拷贝
         Object[] a = c.toArray();
         // 修改次数+1,用于快速失败机制(fail-fast)
         modCount++;
         // 要添加的新元素个数
         int numNew = a.length;
         // 如果要添加的集合为空,直接返回 false
         if (numNew == 0)
             return false;
    
         Object[] elementData; 
         final int s;
         // 如果新元素数量 > 当前数组剩余容量,则需要扩容
         // (s = size) 表示当前已有元素个数
         // (elementData = this.elementData) 表示底层数组
         if (numNew > (elementData = this.elementData).length - (s = size))
             // 扩容到至少能容纳 size + numNew 个元素
             elementData = grow(s + numNew);
    
         // 批量复制:把数组 a 中的所有元素拷贝到 elementData 的末尾位置
         System.arraycopy(a, 0, elementData, s, numNew);
    
         // 更新 size:新的大小 = 原有大小 + 新增元素个数
         size = s + numNew;
    
         // 添加成功,返回 true
         return true;
     }
    

扩容时机一:

  1. 当存满时候,在 add 方法中调用 grow方法会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15.再把所有的元素,全拷贝到新数组中。如果继续添加数据,这个长度为15的数组也满了,那么下次还会继续扩容,还是1.5倍。

扩容时机二:

  1. 一次性添加多个数据,扩容1.5倍不够,怎么办呀?

    如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。

举个例子: 在一开始,如果默认的长度为10的数组已经装满了,在装满的情况下,我一次性要添加100个数据很显然,10扩容1.5倍,变成15,还是不够,

怎么办?

此时新数组的长度,就以实际情况为准,就是110,就看addAll方法,会调用grow方法,然后通过传入的参数得到

4.3.2 LinkedList源码分析:

底层是双向链表结构

核心步骤如下:

  1. 刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null
  2. 添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值
  3. 添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值

暂时跳过了

4.3.3 迭代器源码分析:

迭代器遍历相关的三个方法:

  • Iterator iterator() :获取一个迭代器对象

  • boolean hasNext() :判断当前指向的位置是否有元素

  • E next() :获取当前指向的元素并移动指针

暂时跳过