JavaSE进阶

深入理解JavaSE中final关键字的使用细节,包括其对变量、方法、类的不可变特性,以及final修饰引用的特性和实例变量初始化规则。此外,探讨抽象类的概念与抽象方法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JavaSE进阶

1、final关键字

final是Java语言中的一个关键字,表示最终的,不可变得。final可以修饰变量以及方法还有类等。

final修饰的类不能被继承,修饰的方法不能被重写与覆盖,修饰的变量只能赋值一次。

1.1 final修饰引用问题
package com.zh0u.final关键字;

/*
    final 修饰的变量只能赋值一次。
    “引用”是不是一个变量???答案:是
    
    final 修饰的引用:
    	该引用只能指向1个对象,并且它只能永远指向该对象,无法在指向其它对象。并且在该方法执行过程中,该引用指向对象之后,该对象不会被垃圾回收期回收。知道当前方法执行结束,才会释放空间。
    虽然final的引用指向对象A之后,不能再重新指向对象B,但是对象A内部的数据可被修改。
 */

public class FinalTest02 {
    public static void main(String[] args) {
        Person p1 = new Person(20);
        System.out.println(p1.age); // 20
        p1.age = 100;
        System.out.println(p1.age); // 100

        final Person person = new Person(30);
        person.age = 70;
        System.out.println(person.age);  // 70
    }
}

class Person{
    int age;

    public Person(){}

    public Person(int age){
        this.age = age;
    }
}

image-20211013222817209

1.2 final修饰引用问题

==实例变量如果没有手动赋值,系统会赋默认值。final修饰的实例变量,系统不会赋默认值,要求程序员必须手动赋值。==这个手动赋值,在变量后面直接赋值可以,在构造方法中赋值也可以。

实例变量在对象被new的时候才会赋初始值。

package com.zh0u.final关键字;

public class FinalTest03 {
}

class User{
    // 实例变量
    // 编译器报错
    // final int age;

    // 实例变量
    final double height = 1.8;

    // 以下这堆代码全部联合起来,weight变量也只是赋值了1次。
    // 实例变量
    final double weight = 0;
    // 构造方法
    public User(){
        // 如果这里不写这行代码,系统会默认执行this.weight = 0;这样会报错。
        // 只要赶在系统赋默认值之前赋值就可以。
        // this.weight = 80;
    }
}

1.3 常量

final修饰的实例变量一般添加static修饰,final加static联合修饰的变量成为常量,一般常量变量名全部采用大写,中间使用下划线连接。

常量:实际上常量和静态变量一样,区别在于:“常量的值不能变”,都是存储在方法区,并且都是在类加载是初始化。

常量一般都是公开的。

package com.zh0u.final关键字;

public class FinalTest04 {
    public static void main(String[] args) {
        System.out.println(Chinese.COUNTRY);

        // 常量是无法重新赋值的。
        // Chinese.COUNTRY = "美国";
    }
}

class Chinese{
    // 身份证号,每个人都不一样,对象级别的。
    String idCard;
    // 姓名,对象不同姓名就不同。
    String name;
    // 国家的值是一个固定值:“中国”
    // 实例变量在堆中,一个对象一份。100个对象100份。
    // 实例变量既然使用final修饰了,说明该实例变量值不会随着对象的变化而变化。
    // 该实例变量前面应该添加:static关键字,变为静态的,存储在方法区。
    // final String country = "中国";
    final static String COUNTRY = "中国";
}

1.5 总结
  1. final修饰的类无法继承。
  2. final修饰的方法无法覆盖。
  3. final修饰的变量只能赋值一次。
  4. final修饰的引用一旦指向某个对象,则不能在重新指向其它对象,但该引用指向的对象内部的数据是可以修改的。
  5. final修饰的实例变量必须手动初始化,不能采用系统默认值。
  6. final修饰的实例变量一般和static联合使用,成为常量。

2、抽象类

类到对象是实例化,对象到类是抽象。

2.1 什么是抽象类

类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类,类本身是不存在的,所有抽象类无法创建对象《无法实例化》,所以抽象类是用来被子类继承的,抽象类的子类还可以为抽象类。

抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类使用的(子类在继承的时候会默认调用super()方法来调用父类的构造方法),抽象类的子类既可以是非抽象类也可以是抽象类。

final不能与关键字abstract一起修饰一个类。

image-20211013233831111

抽象类也属于引用数据类型,定义的语法如下:

[修饰符列表] abstract class 类名{

	类体;

}
2.2 抽象方法

抽象类关联到一个概念:抽象方法,什么是抽象方法?抽象方法表示没有实现(没有方法体)的方法,如:

public abstract void doSome();

抽象方法的特点:

  1. 没有方法体也没有花括号,以分号结尾。
  2. 前面修饰符类表中有abstract关键字。

抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中。

package com.zh0u.抽象类;

public class AbstractTest01 {
    public static void main(String[] args) {
        // 抽象类不能被继承
        // new Account();

        // 思考:以下程序能不能使用多态?答案:能
        // 以下的代码就是面向抽象编程思想。
        Account account = new CreditAccount();  // 向上转型,父引用指向子类对象。
        account.doSome(); // 信用卡取款中......
    }
}

abstract class Account{
    public Account(){}  // 这个构造方法由系统系统自动提供,用来供子类使用。
    Account(String name){}

    public abstract void doSome();  // 抽象类中定义了抽象方法,则子类必须对这个抽象方法进行继承。
}


// 非抽象类继承抽象类,必须将父类中的继承过来的抽象类进行重写或实现。
// 原因是子类从父类(抽象类)中继承过来抽象方法只能出现在抽象类中,但是子类不是,所以只能重写。
class CreditAccount extends Account{
    public CreditAccount(){
        super();
    }

    // 子类重写父类(抽象类)中的方法
    public void doSome(){
        System.out.println("信用卡取款中......");
    }
}

// 当然如果子类也是抽象类,那么也可以不去重写父类中的抽象方法。原因是子类继承过来的抽象方法doSome()也是在抽象方法AgriculturalAccount类中。
abstract class AgriculturalAccount extends Account{

}

面试题(判断题):Java语言汇总凡是没有方法体的方法都是抽象方法?

不对,错误的。

Object类中就有很多方法都没有方法体,都是以“;”号结尾,但它们度不是抽象方法,如:

public native int hashCode();

这个方法底层调用了C++写的动态链接库程序,前面的修饰符列表中没有“abstract”,有一个native,表示调用JVM本地程序。

3、接口

接口也是一种引用数据类型,编译之后也会生成一个class字节码文件。接口是完全抽象的或也可以说接口是特殊的抽象类。

3.1 接口的基础语法

接口的定义:

[修饰符列表] interface 接口名{}

接口支持继承和多继承,即一个接口可以继承多个接口,多个接口之间使用“,”分隔开即可。

interface A{}

interface B{}

interface C extends A, B{
    
}

接口中只包含两部分内容,一部分是:常量(值不能改变),另一部分是:抽象方法。

接口中所有的元素都是public修饰的。

接口中的抽象方法定义时:public abstract 修饰符可以省略,编译器编译的时候会自动加上。

接口中的方法都是抽象方法,所有接口中的方法不能有方法体。

类通过implements关键字来对接口的方法进行实现(继承),通过implements实现(继承)的类,必须重写对应接口中的所有抽象方法。

总结

  1. 接口是一种“引用数据类型”。
  2. 接口是完全抽象的。
  3. 接口怎么定义:[修饰符列表] interface 接口名{}
  4. 接口支持多继承
  5. 接口中只有常量+抽象方法
  6. 接口中所有的元素都是public修饰的
  7. 接口中抽象方法的public abstract可以省略。
  8. 接口中常量的public static final可以省略。
  9. 接口中方法不能有方法体。
  10. 一个非抽象的类,实现接口的时候,必须将接口中所有的方法加以实现。
  11. 一个类可以实现多个接口。
  12. extends和implements可以共存,extends在前,implements在后。
  13. 使用接口,写代码的时候,可以使用多态(父类型引用指向子类型对象)。
3.2 面向接口编程

类和类之间叫做继承,类和接口之间叫做“实现”,这里的实现也可以理解为“继承”。

package com.zh0u.接口;

public class InterfaceTest02 {
    public static void main(String[] args) {
        // 访问接口常量
        System.out.println(MyMath.PI);  // 3.1415936

        // 注意接口是无法实例化的,以下代码编译器在编译是会报错
        // new MyMath();

        // 核心思想: 面向接口编程(核心还是多态)
        MyMath myMath = new MyMathImpl();
        System.out.println(myMath.sub(20, 10));
        System.out.println(myMath.sum(20, 10));
    }
}

interface MyMath{
    // 常量 public static final 可以省略
    public static final double PI = 3.1415936;

    // 抽象方法,public abstract 可以省略
    public abstract int sum(int a, int b);

    // 接口中的方法不能有方法体,以下代码会编译器会报错。
    // void doSome(){}

    // 相减的抽象方法
    int sub(int a, int b);
}

class MyMathImpl implements MyMath{  // 类通过implements来实现接口中的方法

    @Override
    public int sum(int a, int b) {  // 这里的public不能省略
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }
}

接口和接口之间支持多继承,那么一个类可以同时实现多个接口吗?答案:可以。

interface W{
    public abstract int m1();
}

interface Y{
    public abstract int m2();
}

interface Z{
    public abstract int m3();
}

class t implements W, Y, Z{

    @Override
    public int m1() {
        return 0;
    }

    @Override
    public int m2() {
        return 0;
    }

    @Override
    public int m3() {
        return 0;
    }
}
3.3 继承和实现连用

extends和implements可以共存,extends在前,implements在后。

public class InterfaceTest04 {
    public static void main(String[] args) {
        // 面向接口编程 ---多态----
        Flyable cat = new Cat();
        cat.fly();  // 飞猫起飞,翱翔太空的一只猫!!!

        Flyable pig = new Pig();
        pig.fly();  // 我是一直会飞的飞猪!!!
    }
}

// 动物类:父类
class Animal{}

// 可飞翔的接口(是一对翅膀)
// 能插拔的就是接口。(没有接口就不能插拔)。
// 接口通常提取的是行为动作。
interface Flyable{
    void fly();
}

// 动物类子类:猫类
// Flyable是一个接口,是一对翅膀的接口,通过接口查到猫身上,让猫变得可以飞翔。
class Cat extends Animal implements Flyable{
    public void fly(){
        System.out.println("飞猫起飞,翱翔太空的一只猫!!!");
    }
}

// 蛇类:如果不想让它飞,可以不实现Flyable接口。
// 没有实现这个接口表示你没有翅膀,没有给你插翅膀,你肯定不能飞。
class Snake extends Animal{

}

// 猪想让做一只飞猪,我们给它插上翅膀即可。
class Pig extends Animal implements Flyable{
    public void fly(){
        System.out.println("我是一直会飞的飞猪!!!");
    }
}

// 这里没有写 extends关键字,但是Fish默认还是要继承Object类
class Fish implements Flyable{
    @Override
    public void fly() {
        System.out.println("我是六眼飞鱼(流言蜚语)!!!");
    }
}
3.4 接口在开发中的作用

接口在开发中的作用,类似于多态在开发中的作用。

**多态:**面向抽象编程,不行面向具体编程。降低程序的耦合度,提供程序的扩展力(OCP)原则。

接口的作用?

接口是完全抽象的,而我们以后正好要求面向抽象编程。面向抽象编程这句话可以修改为:“面向接口编程”。接口的扩展性好,可插拔,符合OCP开发原则。

接口的使用离不开多态机制(接口+多态才可以达到降低耦合度)。

以后进行大项目开发,一般都是讲项目分离成一个模块一个模块的,各模块之间采用接口衔接。降低耦合度。

FoodMenu.java

package com.zh0u.接口在开发中的作用;

/*
    接口:菜单,抽象类
*/

public interface FoodMenu {
    // 西红柿炒蛋
    void shiZiChaoJiDan();

    // 鱼香肉丝
    void yuXiangRouSi();
}

ChineseCooker.java

package com.zh0u.接口在开发中的作用;

public class ChineseCooker implements FoodMenu{
    @Override
    public void shiZiChaoJiDan() {
        System.out.println("中国厨师做的西红柿炒蛋!!!");
    }

    @Override
    public void yuXiangRouSi() {
        System.out.println("中国厨师做的鱼香肉丝!!!");
    }
}

WesternCooker.java

package com.zh0u.接口在开发中的作用;

public class WesternCooker implements FoodMenu{
    @Override
    public void shiZiChaoJiDan() {
        System.out.println("西方厨师做的西红柿炒蛋!!!");
    }

    @Override
    public void yuXiangRouSi() {
        System.out.println("西方厨师做的鱼香肉丝!!!");
    }
}

Customer.java

package com.zh0u.接口在开发中的作用;

// 顾客
public class Customer {
    // 顾客手里有一个菜单
    // Customer has a FoodMenu! 重点:has a
    // 重点:以后凡是能够使用 has a 来描述的,统一以属性的方式存在。
    // 实例变量,属性
    private FoodMenu foodMenu; // 面向接口编程(抽象编程)可以降低程序的耦合度,提高程序的扩展力。

    // 如果以下这样写,就表示写死了(焊接了,没有课插拔了)
    // 中餐厨师
    // ChineseCooker chineseCooker;
    // 西餐厨师
    // WesternCooker westernCooker;

    // 构造方法
    public Customer(){}
    public Customer(FoodMenu foodMenu){this.foodMenu = foodMenu;}

    // setter and getter
    public FoodMenu getFoodMenu() {
        return foodMenu;
    }

    public void setFoodMenu(FoodMenu foodMenu) {
        this.foodMenu = foodMenu;
    }

    // 顾客点菜的方法
    public void order(){
        this.foodMenu.yuXiangRouSi();
        this.foodMenu.shiZiChaoJiDan();
    }
}

/*
Cat is an Animal,但凡是满足is a的表示都可以设置为继承。
Customer has a FoodMenu,但凡是满足 has a 的表示都以属性的形式存在。
*/

Test.java

package com.zh0u.接口在开发中的作用;

public class Test {
    public static void main(String[] args) {
        // 创建厨师对象
        FoodMenu chineseCooker = new ChineseCooker();
        // 创建顾客对象
        Customer customer = new Customer(chineseCooker);
        // 顾客点菜
        customer.order();
    }
}
3.5 类型之间的关系(理解)

is a、has a、like a

is a:

​ Cat is a Animal(猫是一个动物),凡是能够满足is a的表示“继承关系”。

has a:

​ I has a Pen(我有一只笔),凡是能够满足has a关系的表示“关联关系”,关联关系通常以“属性”的形式存在。

like a:

​ Cooker like a FoodMenu(厨师像一个菜单一样),凡是能够满足like a关系的表示“实现关系”,实现关系通常是:类实现接口。

4、抽象类与接口的区别

这里只说一下抽象类和接口在语法上的区别,至于以后抽象类和几口应该怎么进行选择,通过项目区学习。

  1. 抽象类是半抽象的,接口是完全抽象的。
  2. 抽象类中有构造方法,接口中没有构造方法。
  3. 接口和接口之间支持多继承,类和类之间只能单继承。
  4. 一个类可以同时实现多个接口,一个抽象类只能继承一个类。
  5. 接口中只允许出现常量和抽象方法。
  6. 一般抽象类使用的还是少,接口一般都是对“行为”的抽象。

5、Object类

JDK类库的根类 — Object

先研究一下Object,因为这些方法都是所有子类通用的,任何一个类默认继承Object。就算没有直接继承,最终也会间接继承。

Object类当中的那些常用方法,我们可以到哪里去找?

第一种:去源代码中(不推荐,难度较大)。

第二种:去查阅java类库的帮助文档。

目前为止我们只需要知道这几个方法即可:

protected Object clone()   对象克隆

boolean equals(Object obj)   判断两个对象是否相等

int hashCode()   获取对象哈希值的一个方法

protected void finalize()    垃圾回收器负责调用的方法

String toString()   将对象转换为字符串形式
5.1 toString方法

源代码

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

源代码上toString()方法的默认实现是:类名@对象的内存地址转换为十六进制的形式。

toString的作用是通过调用这个方法可以将一个“Java对象”转换成“字符串”的形式输出。

SUN公司开发Java语言的时候,建议所有的子类都去重写toString()方法,toString()方法应该是一个简洁的、详实的、容易阅读的。

注意:Java在输出引用的时候,如果不调用toString()方法,也会默认调用它。

package com.zh0u.Object;

public class Object_toString {
    public static void main(String[] args) {
        System.out.println(new definedDate(1970, 1, 1));  // 直接输出引用,默认也会调用 toString() 方法
    }
}

class definedDate{
    int year, month, day;
    public definedDate(){}
    public definedDate(int year, int month, int day){
        this.year = year;
        this.month = month;
        this.day = day;
    }

    @Override
    public String toString() {  // 重新父类中的 toString() 方法
        return "definedDate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }
}
5.2 equals方法

源代码:

public boolean equals(Object obj) {
        return (this == obj);
    }

这个方法是Object类默认实现的,判断两个java对象是否相等,不能使用“ == ” ,因为“==”比较的是引用的内存地址是否相同。

package com.zh0u.Object;

public class Object_equals {
    public static void main(String[] args) {
        // 基本数据类型
        // 判断两个基本数据类型的数据是否相等直接使用“==”就可以了。
        int a = 200, b = 200;
        // 这个“==”是判断 a 中保存的值和 b 中保存的值是否相等。
        System.out.println(a == b);  // true
        // 判断两个java对象是否相等,应该怎么比较?能直接使用“==”吗?
        // 创建两个日期对象是:2008年8月8日
        definedDate d1 = new definedDate(2008, 8, 8); // d1 = 0x1234
        definedDate d2 = new definedDate(2008, 8, 8); // d2 = 0x4567
        definedDate d3 = new definedDate(2008, 4, 5); // d3 = 0x8902
        definedDate d4 = null;
        // 测试一下,比较两个对象是否相等,不能使用“==”。
        // 这里的“==”判断的是:d1中保存的对象内存地址和d2中保存的内存地址是否相等
        System.out.println(d1.equals(d2)); // true
        System.out.println(d2.equals(d3)); // false
        System.out.println(d1.equals(d4)); // false
    }
}
// 重写方法:复制粘贴。相同的返回值类型、相同的方法名、相同的形式参数列表。
// 通过重写Object中的equals方法,在使用equals进行对象比较的时候,应该比较对象中的值,而不是引用的内存地址。
// 如下:如果两个变量的日期相同,在调用equals进行比较的时候应该返回true,反之返回false。
@Override
public boolean equals(Object o) {
    /*  // idea编译器自动生成
        if (this == o) return true; // 首先比较内存地址
        if (o == null || getClass() != o.getClass()) return false; // 比较传入的对象是否为空或类名是否不相等
        definedDate that = (definedDate) o;
        return year == that.year && month == that.month && day == that.day;
        */
    if (o == null) return false;  // 如果 比较的对象是空,则直接返回 false。
    // 内存地址相同的时候指向的堆内存的对象肯定是同一个对象。
    if (this == o) return true; // 如果两个内存地址相同,则直接返回true。
    if (o instanceof definedDate){  // 直接先先比较类是否相同,如果不相同直接返回false
        definedDate date = (definedDate) o;
        return this.year == date.year && this.month == date.month && this.day == date.day;
    }
    return false;
}

在进行字符串比较的时候应该使用以下的方式二:

方式一:if (username.equals("admin")); // 可能会出现空指针异常,建议使用方式二
方式二:if ("admin".equals(username)); // 有效避免空指针异常
5.3 String类!

Java中的String类也是引用数据类型,在比较两个字符串是否相同的时候,应该使用equals()方法而不是“==”。

注意:"abc"这是一个字符串对象,字符串在Java中有优待,不需要new也是一个对象。

package com.zh0u.Object;

// java语言当中的字符串String重写了toString()和equals()方法。
/*
重要结论:
    java中所有的基本数据类型可以使用“==”进行比较。而所有的引用数据类型使用“equals方法”进行比较。
*/
public class Object_String {
    public static void main(String[] args) {
        // 大部分情况下,采用这样的方式创建字符串对象。
        String s1 = "hello";
        String s2 = "abc";

        // 实际上String也是一个类。不属于基本数据类型,既然String是一个类,那么一定存在构造方法。
        String s3 = new String("test01");
        String s4 = new String("test01");
        // new两次产生两个对象内存地址,s3保存的内存地址和s4保存的内存地址不同。
        // “==”比较的是对象的内存地址,而不是对象的内容。因此不能使用“==”比较两个字符串的值。
        System.out.println(s3 == s4);
    }
}
5.4 equals 深入理解
package com.zh0u.equals深层次理解;

// 重点:在进行 equals() 方法重写的时候一定要彻底。

public class Test01 {
    public static void main(String[] args) {
        User u1 = new User("zhangsan", new Address("beijing", "daxing district", "0x12345"));
        User u2 = new User("zhangsan", new Address("beijing", "daxing district", "0x12345"));
        User u3 = new User("lisi", new Address("chaoyang", "gongan district", "0x1256"));
        System.out.println(u1.equals(u2)); // true
        System.out.println(u1.equals(u3)); // false
    }
}

class User{
    String name;
    Address address; // 用户住址

    public User(){
    }

    public User(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", address=" + address +
                '}';
    }

    // 重写 equals() 方法
    // 规则:当一用户的用户名和家庭住址都相同时表示同一个用户。
    // 这个 equals() 判断的是User对象和User对象是否相等。
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        // 注意:这里在进行用户地址比较的时候应该调用在Address中已经重写了的equals,而不是调用Object中的equals方法。
        // 即 equals() 方法重写的时候一定要彻底。
        return this.address.equals(user.address) && this.name.equals(user.name);
    }
}


class Address{
    String city;  // 城市
    String district; // 区
    String zipcode; // 邮政编码

    // 构造方法
    public Address() {
    }
    public Address(String city, String district, String zipcode) {
        this.city = city;
        this.district = district;
        this.zipcode = zipcode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        return this.city.equals(address.city) && this.district.equals(address.district)
                && this.zipcode.equals(address.zipcode);
    }
}
5.5 finalize方法(了解)

关于Object类中的finalize()方法:

  1. 在Object类中的源代码:

    protected void finalize() throws Throwable { }
    

    GC: 负责调用finalize()方法

  2. finalize()方法只有一个方法体,里面没有代码,而且这个方法是protected修饰的。

  3. 这个方法不需要程序员手动调用,JVM的垃圾回收器负责调用这个方法,不像equals、toString的方法是需要写代码调用。

  4. finalize()方法的执行时机:当一个java对象即将被垃圾回收的时候,垃圾回收器负责调用finalize()方法。

  5. finalize()方法实际上是SUN公司为java程序员准备的一个时机(如static代码块),垃圾销毁时机。如果希望在对象在销毁时机执行一段代码代码的话,这段代码要写到finalize()方法当中。

  6. 提示:java中的哪里回收器不会轻易启动,如果垃圾太少、或者时间没到等种种条件下,有可能启动,也有可能不启动。

package com.zh0u.Object;

public class Object_finalize {
    public static void main(String[] args) {
        // 创建对象
        Person p = new Person();
        // 把 p 变成垃圾
        p = null; 
        
        // 上面的垃圾太少,可能不会是GC启动,下面通过for循环来制造大量的垃圾。
        for (int i = 0; i < 100000000; i++){
            Person c = new Person();
            c = null;
            // 也可以通过以下代码来“建议”启动GC回收器。
            System.gc();
        }
    }
}

/*
 项目开发中有这样的业务需求:所有对象在JVM中被释放的时候,请记录一下释放的时间!!!
 记录对象被释放的时间点,这个负责记录的代码就可以写到finalize()方法中。
*/
class Person{
    // 重写finalize()方法
    // Person类型的对象被垃圾回收器回收的时候,垃圾回收器负责调用:p.finalize();
    @Override
    protected void finalize() throws Throwable {
        System.out.println(this + "即将被销毁!!!");
    }
}
5.6 hashCode方法
package com.zh0u.Object;

/*
 * hashCode方法:
 *  在Object中的hashCode方法的源代码:public native int hashCode();
 *  这个方法不是抽象方法,带有native关键字,底层调用C++程序。
 * hashCode()方法返回的是哈希码:实际上就是一个Java对象的内存地址,经过哈希算法得出的一个值。
 * 所以hashCode()方法的执行结果可以等同看作一个Java对象的内存地址。
 */
public class Object_hasCode {
    public static void main(String[] args) {
        Object o = new Object();
        // 对象内存地址经过哈希算法转换的一个数字。可以等同看做内存地址。
        System.out.println(o.hashCode());  // 1836019240
    }
}

建议可以看一下“深克隆与”浅克隆“。

补充:什么是API?

应用程序编程接口,整个JDK的类库就是一个javase的API,每个API都会配置一套API帮助文档。

6、package和import

6.1 关于Java语言的package和import机制:
  1. 为什么使用package?

package是java中包机制。包机制的作用是为了方便程序的管理。不同功能的类分别存在在不同的包下(按照功能划分,不同软件包具有不同的功能)。

  1. package的怎么用?

package是一个关键字,后面加包名,例如:package com.zh0u.javase.interfacetest;

注意:package语句只允许出现在java源代码的第一行。

  1. 包名命名规范?

一般都采用公司域名倒序的方式(因为公司域名具有唯一性)。命名规范为:

公司域名倒序 + 项目名 + 模块名… [ + 功能名 ]

  1. 对于带有package的java程序怎么编译?怎么运行?

采用之前的编译方式和运行不行了,类名不再是:HelloWorld了,类名是:com.zh0u.javase.import_test.HelloWorld

编译:javac -d . HelloWorld.java。-d 带包编译,.代表编译之后生成的东西放到当前目录下(点代表当前目录)。

运行:java com.zh0u.javase.import_test.HelloWorld

package com.zh0u.interface_test;

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world!");
    }
}
6.2 import
  1. import什么时候使用?

在A类中使用B类,如果A类和B类在同一个包下,不需要import,否则就需要使用import。

  1. import的用法。

import语句只能出现在package语句之下,class声明之上。

import语句还可以使用*的方式导入类。

import com.zh0u.接口.*;

java.lang包下面的直接类不需要使用import导入,直接可以使用。如:String.java。

7、访问权限控制

Java访问级别修饰符主要包括:private、protected 和 public ,可以限定其他类对该类、属性和方法的使用权限。

修饰符类的内部同一个包里子类任何地方
privateYNNN
defaultYYNN
protectedYYYN
publicYYYY

注意以上对类和接口的修饰只有 publicdefault ,内部类除外。

范围从大到小排序:public > protected > default > private

package com.zh0u.访问控制权限;

public class User {
    // 给一些属性
    // 私有
    private int id;
    // 受保护
    protected int age;
    // 公开
    public int weight;
    // 默认
    String name;

    // 方法
    public void m1(){}
    private void m2(){}
    void m3(){}
    protected void m4(){}

    // 静态方法也可以使用四种访问权限修饰符
}

// java: 此处不允许使用修饰符private
/*
private class J{}
*/

// java: 此处不允许使用修饰符protected
/*protected class J{}*/

// default
class J{}

8、匿名内部类

package com.zh0u.匿名内部类;

/**
 * 匿名内部类:
 *      1. 什么是内部类?
 *          内部类:在类的内部有定义了一个新的类。被称为内部类。
 *      2. 内部类的分类:
 *          静态内部类:类似于静态变量
 *          实例内部类:类似于实例变量
 *          局部内部类:类似于局部变量
 *      3. 使用内部类编写的代码可读性很差。能不用尽量不用。
 *      4. “匿名内部类”是局部内部类的一种,因为这个类没有名字而得名,叫做匿名内部类。
 */
public class Test01 {
    // 该类在类的内部,所有成为内部类
    // d由于前面有static,所有成为“静态内部类”
    static class Inner1{}

    // 实例内部类
    class Inner2{}

    // 方法
    public void doSome(){
        class Inner3{} // 局部内部类
    }

    public void doOther(){
        // doSome()方法中的局部内部类Inner3在doOther中不能调用。
    }

    // main()方法,入口
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        // new ComputeImpl()这里引用多态,父类型引用指向子类型对象
        myClass.mySum(new ComputeImpl(), 100, 200);
    }
}

// 负责计算的接口
interface Compute{
    int sum(int a, int b);
}

// 实现Compute接口
class ComputeImpl implements Compute{

    @Override
    public int sum(int a, int b) {
        return a + b;
    }
}

class MyClass{
    // 数学求和方法
    public void mySum(Compute c, int x, int y){
        int retValue = c.sum(x, y);
        System.out.println(x + "+" + y + "=" + retValue);
    }
}

实现匿名内部类:

// main()方法,入口
public static void main(String[] args) {
    MyClass myClass = new MyClass();
    // new ComputeImpl()这里引用多态,父类型引用指向子类型对象
    // myClass.mySum(new ComputeImpl(), 100, 200);

    // 匿名内部类,表示这个ComputeImpl没有名字了,这里表面看上去是接口可以直接new了,实际上并不是接口可以new了。
    // 后面的“{}”代表对接口的实现。另外不建议使用匿名内部类,因为一个类没有名字名字就无法重复使用,而且代码太乱,可读性太差。
    myClass.mySum(new Compute(){
        public int sum(int a, int b){
            return a + b;
        }
    }, 200 ,300);
}

9、数组

9.1 数组的概述
1. Java语言中的数组是一种引用数据类型。不属于基本数据类型,数组的父类是Object。
2. 数组实际上是一个容器,可以同时容纳多个元素。(数组是一组数据的集合)。
3. 数据当中可以存储“基本数据类型”的数据,也可以存放“引用类型的”数据。
4. 数组因为是引用类型,所有数组对象是堆内存当中。(数组是存储在堆当中的)。
5. 数组当中如果存储的是“java对象”的话,实际上存储的是对象的“引用(内存地址)”。
6. 数组一旦创建,在java中规定,长度不可变(Java数组的长度不可变)。
7. 数组的分类:一维数组、二维数组、多维数组……
8. 所有的数组对象都有length属性(Java自带),用来获取数组中的元素个数。
9. Java中的数组要求数组中的元素的类型统一。比如int类型数组,就只能存储int类型。
10. 数组在内存方面存储的时候,数组的元素内存地址是连续的,数组实际上是一个简单的数据结构。
11. 数组中首元素的内存地址作为整个数组对象的内存地址。
12. 数组数据结构的优点和缺点是什么?
     优点:查询、查找、检索某个下标上的元素时效率极高,可以说是查询效率最高的一个数据结构。
         为什么检索效率高?
         第一:每个元素的内存地址在空间存储上是连续的。
         第二:每个元素类型相同,所有占用空间大小一样。
         第三:知道第一个元素内存地址,知道每个元素占用空间的大小,又知道下标,所以通过数学表达式就可以计算出某个下标上元素的内存地址,直接通过内存地址定位元素,所以数组的检索效率是最高的。
         数组中存储100个元素,或者存储100万个元素,在元素查询、检索方面,效率是相同的,因为数组中元素查找的时候不会一个一个找,直接通过数学表达式计算出来,然后直接在内存中进行定位。
     缺点:
         第一:由于为了保证数组中每个元素的内存地址连续,所有在数组上随机删除或者增加元素的的效率较低。因为随机删除元素会涉及到后面元素统一向前或者向后唯一的操作。
         第二:数组不能存储大数据量,因为很难在内存空间上找到一块特别大的连续的内存空间。

注意:对于数组中最后一个元素的增删,是没有效率上的影响的。
	
9.2 一维数组内存结构图

image-20211020193709923

9.3 一维数组的定义与初始化
13. 怎么声明/定义一个一维数组?
     语法格式如下:
         int[] array1;
         double[] array2;
         boolean[] array3;
         String[] array4;
         Object[] array5;
14. 怎么初始化一个一维数组?
     包括两种方式:“静态初始化”和“动态初始化”。
     静态初始化:
         int[] array1 = {100, 20, 30, 3};
         char[] chars = new char[]{'j','a','v','a'};
     动态初始化:
         int[] array2 = new int[5];  // 这里的5表示数组的元素个数位5,初始化int类型数组,每个元素默认为0。

静态方式:

public static void main(String[] args) {
        // 声明一个int类型的数组,使用静态初始化的方式
        int[] array = {1, 200, 3, 5, 8}; // C++风格为:int array[] = ....
        
    	// 注意:使用这种方式初始化的时候,new后面不能指定数组的长度。
        char[] chars = new char[]{'j','a','v','a'};  
    
    	// 所有的数组对象都有length属性
        System.out.println("数组中元素的个数位 = " + array.length);

        // 数组中一个元素都有下标,通过下标对数组中的元素进行存取。
        System.out.println("第一个元素 = " + array[0]);
        System.out.println("最后一个元素 = " + array[4]);
        System.out.println("最后一个元素 = " + array[array.length - 1]);

        // 修改第一个元素的值。
        array[0] = 2000;
        System.out.println("修改后,第一个元素的值 = " + array[0]);
    }

动态方式:

public class ArrayTest02 {
    public static void main(String[] args) {
        // 动态方式初始化
        float[] a = new float[5];
        for (float c : a) {
            System.out.println(c);
        }
    }
}
9.4 数组形参
package com.zh0u.javase.array;

public class ArrayTest03 {
    public static void main(String[] args) {
        printArray(new int[3]);
        String[] names = new String[3];
        names[0] = "java";
        names[1] = "python";
        names[2] = "php";
        printArray(names);
    }

    // 数组形参
    public static void printArray(int[] args){
        for (int arg: args){
            System.out.print(arg + " ");
        }
        System.out.println();
    }
    public static void printArray(String[] args){
        for (String arg: args){
            System.out.print(arg + " ");
        }
        System.out.println();
    }
}
9.5 main方法
package com.zh0u.javase.array;

/*
 * 1. main方法上面的“String[] args”有什么用?
 *      分析:JVM负责调用main方法,在调用main方法的时候会自动传一个String数组过来。
 */
public class ArrayTest04 {
    // 这个方法程序员负责编写,JVM负责调用。JVM调用的时候一定会传一个String数组过来。
    public static void main(String[] args) {
        // JVM默认传过来的合格数组对象的长度为 0,且通过测试得知 args 不是 null。
        System.out.println("JVM传过来的String数组参数,他这个数组的长度 = " + args.length);

        // 以下这一行代码表示的含义:数组对象创建了,但是数组中没有任何数据。
        // String[] s = new String[0];
        // String[] s = {}; //静态初始化数组,里面没有东西。
        //printLength(s);

        // 这个数组什么时候里面会有值呢?
        // 其实这个数组是留个用户的,用户可以在控制台上输入参数,这个参数会自动被转换为“String[] args”。
        // (当然这里也可使用idea中找到edit configuration中的program arguments进行传递参数。)
        // 例如:java ArrayTest05 abc def xyz,那么这个时候JVM会自动将“abc、def、xyz”通过空格的方式进行分离,分离完成之后,自动放入String[] args数组里面。
        // 这样就String[] args数组中的元素便为:{"abc","def","xyz"};
        for (String arg: args){
            System.out.print(arg + " ");
        }
    }
    public static void printLength(String[] strings){
        System.out.println(strings.length);
    }
}
9.6 一维数组的深入研究
package com.zh0u.javase.array;

/*
    一维数组的深入,数组中存储的类型为:引用数据类型
 */
public class ArrayTest05 {
    public static void main(String[] args) {
        // a是一个数组,里面存放着基本数据类型int
        int[] t = {20, 30, 44};

        // 创建一个Animal类型的数组
        Animal[] a1 = {new Animal(), new Animal()};

        // Animal数组中不能存放Product类型的对象,以下代码会报错。
        // Animal[] animals = {new Product()};

        // Animal类型的数组中可以存放子类对象
        Animal[] a2 = {new Cat(), new Bird()};
        for (Animal a: a2){
            a.move();
        }

        // 使用多态时,当调用子类中特有的方法时,必须向下转型
        for (Animal a: a2){
            if (a instanceof Cat){
                ((Cat) a).catchMouth();
            } else if (a instanceof Bird){
                ((Bird) a).sing();
            }
        }
    }
}

// 动物类
class Animal{
    public void move(){
        System.out.println("Animal move...");
    }
}

// 商品类
class Product{}

class Cat extends Animal{
    @Override
    public void move() {
        System.out.println("猫仔走猫步!!!");
    }
    public void catchMouth(){
        System.out.println("好猫猫,抓老鼠!!!");
    }
}

class Bird extends Animal{
    @Override
    public void move() {
        System.out.println("鸟儿在歌唱!!!");
    }
    public void sing(){
        System.out.println("美丽的鸟儿在唱歌!!!");
    }
}
9.7 一维数组的扩容

关于一维数组的扩容,在Java开发中,数组的长度一旦确定是可变的,如果数组满了,这是就需要扩容,可以使用System.arraycoepy()进行拷贝。

Java对数组的扩容是:先新建一个大容量的数组,然后将小容量数组中的数据一个一个拷贝到大数组当中。数组的扩容效率较低,因为涉及到拷贝的问题,所以在开发中尽可能少的拷贝数组,可以在创建数组对象的时候预估计一下多长合适,最好预估准确一点,这样可以避免数组的扩容次数,提高效率。

package com.zh0u.javase.array;

public class ArrayTest06 {
    public static void main(String[] args) {
        // java中的数组是怎么进行拷贝的呢?
        // System.arraycopy(拷贝源,拷贝的起始位置,目标数组,数组的起始位置,长度);

        // 拷贝源(从这个数组中拷贝)
        int[] src = {1, 11, 22, 3, 4};

        // 拷贝目标(拷贝到这个目标数组上)
        int[] dest = new int[10]; // 动态初始化

        // 调用JDK System类中的arraycopy方法来完成数组的拷贝
        System.arraycopy(src, 1, dest, 3, 2);
        // 拷贝所有的元素
        // System.arraycopy(src, 0, dest, 0, src.length);

        // 遍历数组
        for (int d: dest){
            // 0  0  0  11  22  0  0  0  0  0
            System.out.print(d+ "  ");
        }
        System.out.println();

        // 数组中如果存储的元素时引用,可以拷贝吗?答案:当然可以。
        String[] strings = {"java", "study", "php", "oracle"};
        String[] newStrings = new String[5];
        System.arraycopy(strings, 0, newStrings, 0, strings.length);
        for (String tmp: newStrings){
            System.out.print(tmp + " "); // java study php oracle null 
        }
    }
}

数组拷贝内存图:

image-20211021201722613

9.8 二维数组

二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个“一维数组”。二维数组的静态初始化为:

int[][] array = {{1,2,3}, {4,5,6},{7,8,9}};	
package com.zh0u.javase.array;

public class ArrayTest07 {
    public static void main(String[] args) {
        // 一维数组
        int[] array = {100, 200, 300};

        // 二维数组,里面四个一维数组
        int[][] a = {{1,22,3,60}, {45,5,64}, {7,68,69}, {0}};

        // 三维数组
        // int[][][] b = {{{1,2,3},{0,1,1}},{{0,0,0}}};
        // System.out.println(b[0][0][1]);  // 2

        // 二维数组元素的“存”与“改”
        // 取出二维数组 a 的第一个一维数组
        int[] a0 = a[0];
        // System.out.println(a0[0]); // 1

        // 动态初始化二维数组
        // int[][] arr = new int[3][4];

        printArray(a);
        // printArray(new int[][]{{1,22,3,60}, {45,5,64}, {7,68,69}, {0}});

    }
    // 打印二维数组
    public static void printArray(int[][] arr){
        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();
        }
    }
}

作业题一:

image-20211021205132237

package com.zh0u.javase.array.task02;

public class MyStack {
    // 使用一维数组模拟栈存储元素的方法
    // 使用Object数组,表示什么都可以往里面放
    private Object[] elements;
    // 栈帧
    private int index;

    public MyStack() {
        elements = new Object[10];  // 初始化指定一维数组的长度为 10
        index = -1; // 初始化栈帧,栈帧为 -1 表示刚开始的时候不存在
    }

    public Object[] getObjects() {
        return elements;
    }

    public void setObjects(Object[] objects) {
        this.elements = objects;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    /**
     * 模拟压栈动作。当栈帧大于等于数组长度-1时,表示压栈失败。
     * @param obj 需要压入栈的元素。
     */
    public void push(Object obj){
        if (this.index >= elements.length - 1){
            System.out.println("压栈失败!栈已满!");
            return;
        } else {
            System.out.println("压栈成功!");
        }
        elements[++index] = obj;  // 将元素压入栈
    }

    /**
     * 模拟弹栈动作,当栈帧小于0时,表示弹栈失败。
     * @return 弹栈是否成功
     */
    public boolean pop(){
        if (this.index < 0){
            return false;
        }
        elements[index--] = null;  // 弹出栈元素
        return true;
    }
}

image-20211021205601591

Room.java

package com.zh0u.javase.array.task01;

import java.util.Scanner;

public class Room {
    private String roomId; // 房间编号
    private String roomType; // 房间类型
    private int isFree; // 是否空闲 1 表示空闲,2表示非空闲

    public Room(){}

    public Room(String roomId, String roomType, int isFree) {
        this.roomId = roomId;
        this.roomType = roomType;
        this.isFree = isFree;
    }

    public String getRoomId() {
        return roomId;
    }

    public void setRoomId(String roomId) {
        this.roomId = roomId;
    }

    public String getRoomType() {
        return roomType;
    }

    public void setRoomType(String roomType) {
        this.roomType = roomType;
    }

    public int getIsFree() {
        return isFree;
    }

    public void setIsFree(int isFree) {
        this.isFree = isFree;
    }

    // 房屋预定
    public boolean chickIn(Room[][] rooms){
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要入住的房间编号: ");
        String chickInRoomID = scanner.next();
        for (Room[] room : rooms) {
            for (Room value : room) {
                if (value.getRoomId().equals(chickInRoomID)) {
                    if (value.getIsFree() == 2) return false;  // 如果房间不是空闲的,直接返回 false
                    value.setIsFree(2); // 设置入住
                    return true;
                } else {
                    System.out.println("输入编号的房间不存在!");
                }
            }
        }
        return false;
    }// 退房
    public boolean chickOut(Room[][] rooms){
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输需要退房的房间编号: ");
        String chickInRoomID = scanner.next();
        for (Room[] room : rooms) {
            for (Room value : room) {
                if (value.getRoomId().equals(chickInRoomID)) {
                    if (value.getIsFree() == 1) return false;  // 如果房间是空闲的,直接返回 false
                    value.setIsFree(1); // 设置退房
                    return true;
                }
            }
        }
        return false;
    }

    // 打印所有的房间、包括状态
    public void printRoomStatus(Room[][] rooms){
        System.out.println("----------房间编号--------------类型----------------是否空闲-------------");
        for (Room[] room : rooms) {
            for (Room value : room) {
                String tmp = null;
                if (value.getIsFree() == 2){
                    tmp = "非空闲";
                } else if (value.getIsFree() == 1){
                    tmp = "空闲";
                }
                System.out.printf("%15s %17s %16s", value.roomId, value.roomType, tmp);
                System.out.println();
            }
            System.out.println();
        }
    }

    // 创建酒店房间
    public Room[][] createRoom(int floor, int numberOfRooms){
        // 动态定义酒店的大小
        Room[][] rooms = new Room[floor][numberOfRooms];
        // 创建酒店的间房
        for (int i = 0; i < floor; i++){
            for (int j = 0; j < numberOfRooms; j++){
                rooms[i][j] = new Room();  // 必须创建Room对象
                rooms[i][j].roomId = i+1 + "000" + j;
                rooms[i][j].roomType = "豪华房";
                rooms[i][j].isFree = 1;
            }
        }
        /*
        for (int i = 0; i < floor; i++){
            rooms[i+1][i].roomType = "普通房";
            rooms[i+1][i].roomType = "双人间";
            rooms[i+1][i].roomType = "豪华版";
        }
         */
        return rooms;
    }
    // 输出帮助
    public void help(){
        System.out.println("exit -- 退出系统");
        System.out.println("chickin -- 入住酒店");
        System.out.println("chickout -- 退房");
        System.out.println("showroom -- 输出酒店房间信息");
    }
}

test.java

package com.zh0u.javase.array.task01;

import java.util.Scanner;

/**
 * 酒店管理系统
 */
public class task01 {
    public static void main(String[] args) {
        // 创建酒店
        // 酒店总层数,每层房间数
        int floor = 3, numberOfRoom = 5;
        Room room = new Room();
        Room[][] rooms = room.createRoom(floor, numberOfRoom);
        System.out.println("**************************欢迎入住 蓬莱大酒店 **************************");
        while (true){
            System.out.print("请输入指令(输入help获取帮助): ");
            Scanner scanner = new Scanner(System.in);
            String instruct = scanner.next();
            if ("exit".equals(instruct)) break;
            else if ("help".equals(instruct)) room.help();
            else if ("showroom".equals(instruct)) room.printRoomStatus(rooms);
            else if ("chickin".equals(instruct)) {
                if (room.chickIn(rooms)){
                    System.out.println("入住成功!!!");
                } else {
                    System.out.println("当前房间已经被占用,请换用其他房间!!!");
                }
            }
            else if ("chickout".equals(instruct)){
                if (room.chickOut(rooms)){
                    System.out.println("退房成功!!!");
                } else {
                    System.out.println("退房失败!!!");
                }
            }
        }
    }
}

10、常见算法

排序算法:冒泡排序算法、选择排序算法。

查找算法:二分法查找

以上算法在以后的Java实际开发中我们不需要使用,因为Java已经封装好了,直接调用就可以,只不过以后面试的时候可能会碰上。

Java中提过了一个数组工具类:java.util.Arrays,Arrays是一个工具类,其中有个sort()静态方法,可以排序,直接使用类名调用就可以。

10.1 冒泡排序
package com.zh0u.javase.常见算法;

/**
 * 冒泡排序算法
 * int[] a = {4,6,3,2,1}
 * 每一次循环结束之后,都要找出最大的数据,放到参与比较的这堆数据的最右边。(冒出最大的那个气泡)。
 * 循环的次数为 a.length - 1;由于每次都要找出最大的那个值,所以最大参与比较的元素都会少一个。
 *
 * 过程如下:
 * 参与第一遍循环的数据:4,6,3,2,1
 * 4,6,3,2,1  第一次:4与6,不交换
 * 4,3,6,2,1  第二次:6与3,交换
 * 4,3,2,6,1  第三次:6与2,交换
 * 4,3,2,1,6  第四次:6与1,交换
 *
 * 参与第二遍循环的数据(由于第一遍的时候已经找出了最大值6,所以它不需要再次参加比较):4,3,2,1
 * 3,4,2,1  第一次:4与3比较,交换
 * 3,2,4,1  第二次:4与2比较,交换
 * 3,2,1,4  第三次:4与1比较,交换
 *
 * 参与第三遍循环的数据:3,2,1
 * 2,3,1  第一次:3与2比较,交换
 * 2,1,3  第二次:3与1比较,交换
 *
 * 参与第四遍循环的数据:2,1
 * 1,2 第一次:2与1比较,交换
 *
 * 方法一:
 * for (int i = 0; i < a.length -1; i++){
 *             for (int j = 0; j < a.length - 1 - i; j++){
 *
 *             }
 * }
 *
 * 方法二:(推荐)
 * for (int i = a.length - 1; i > 0; i--){
 *             for (int j = 0; j < i; j++){
 *                  // 这里通过异或来进行两个变量值的交换
 *                  a[j] = a[j] ^ a[j+1];
 *                  a[j+1] = a[j+1] ^ a[j];
 *                  a[j] = a[j] ^ a[j+1];
 *             }
 * }
 */
public class BubbleSort {
    public static void main(String[] args) {
        int[] a = {4,6,3,2,1}; // length = 6
        int count = 0;  // 比较次数
        for (int i = 0; i < a.length -1; i++){
            for (int j = 0; j < a.length - 1 - i; j++){
                count++;
                if (a[j] > a[j+1]){
//                    int tmp = a[j];
//                    a[j] = a[j+1];
//                    a[j+1] = tmp;
                    a[j] = a[j] ^ a[j+1];
                    a[j+1] = a[j+1] ^ a[j];
                    a[j] = a[j] ^ a[j+1];
                }
            }
        }
        System.out.println("比较次数为: " + count);  // 10
        for (int v: a){
            System.out.print(v + " ");
        }

//        使用异或进行两个整数的交换
//        int c = 2, d = 3;
//        c = c ^ d;
//        d = d ^ c;
//        c = c ^ d;
//        System.out.println(c + ", " + d);
    }
}
10.2 选择排序
package com.zh0u.javase.常见算法;

/**
 * 选择排序:
 * 选择排序比冒泡排序的效率高,高在交换位置的次数上,选择排序的排序位置是有意义的。冒泡排序的比较次数与选择排序的比较次数是相等的。
 * 循环一次,然后找出参加比较的这堆数据中最小的数据,拿着这个最小的值和前面的数据进行“交换数据”。
 *
 * 这里以:3, 1, 6, 2, 5为例
 * 第一次循环参加比较的元素为:3, 1, 6, 2, 5
 * 这里先假定最左边的元素 3 是最小的,依次和后面的是个元素相比较。
 * 步骤:
 *      1. i = 0;  min = 0;  内层循环进入第一次:
 *          ① j = 0 + 1; a[j] = 1; a[min] = 3; a[j] < a[min]; min = j; min = 1;
 *          ② j = 2;     a[j] = 6; a[min] = 1; a[j] < a[min];不成立
 *          ③ j = 3;     a[j] = 2; a[min] = 1; a[j] < a[min];不成立
 *          ④ j = 4;     a[j] = 4; a[min] = 1; a[j] < a[min];不成立
 *         min != i 成立,交换值:  tmp = a[i]; a[i] = a[min]; a[min] = tmp; 即 - a[0] = 1; a[1] = 3;
 * 变成:1, 3, 6, 2, 5
 *
 * 第二次循环参加比较的元素为:3, 6, 2, 5
 * 找出 2 是最小的元素,将 2 和 3 的位置交换
 * 变成:1, 2, 6, 3, 5
 *
 * 第三次循环参加比较的元素为:6, 3, 5
 * 找出 3 是最小的元素,将 3 和 6 的位置交换。
 * 变成:1, 2, 3, 6, 5
 *
 * 第四次循环参加比较的元素为:6, 5
 * 找出 5 是最小的元素,将 5 和 6 的位置交换。
 * 变成:1, 2, 3, 5, 6
 */
public class SelectSort {
    public static void main(String[] args) {
        int count1 = 0; // 比较次数
        int count2 = 0; // 交换次数
        int[] a = {3, 1, 6, 2, 5, 9, 7};
        // 选择排序
        // 五条数据循环4次(外层循环4次)
        for (int i = 0; i < a.length - 1; i++){
            // i的值为 0 1 2 3
            // i正好是“参加比较的这堆数据中”最左边的那个元素的下标
            // i是一个参与比较的这堆数据中的起点下标。
            // 假设起点i下标位置上的元素时最小的。
            int min = i;
            for (int j = i + 1; j < a.length; j++){
                // j 的值是 1 2 3 4
                // System.out.println("----->" + j);
                count1++;
                if (a[j] <a[min]){
                    min = j;  // 最小值的元素下标是 j
                }
            }
            // 当i和min相等是,表示最初猜测是对的。
            // 当i和min不相等是,表示最初猜测是错的,有比这个元素更小的元素,需要那这个更小的元素和最左边的元素交换位置。
            if (min != i){
                // 值交换
                a[min] = a[min] ^ a[i];
                a[i] = a[i] ^ a[min];
                a[min] = a[min] ^ a[i];
                count2++;
            }
        }
        for (int i: a){
            System.out.print(i + " ");  // 1 2 3 5 6
        }
        System.out.println();
        System.out.println("比较次数" +count1);
        System.out.println("交换次数" +count2);
    }
}

image-20211023210459617

10.3 二分法查找
package com.zh0u.javase.常见算法;

/*
    数组的元素查找
        数组元素查找有两种方式:
            1. 一个一个挨着找,知道找到为止。
            2. 二分法查找(算法),效率较高。
 */
public class OrdinaryAlgorithm {
    // 第一种方法
    /**
     * 检索某个特定元素第一次出现在特定数组中的位置,由于是一个一个紧挨着找,这种方法的效率比较低。
     * @param array 传入的数组。
     * @param element 需要查找(检索)的元素。
     * @return 当找到对应的元素时,返回响应的下标。否则返回 -1.
     */
    public static int ordinarySearch(int[] array, int element){
        for (int i = 0; i < array.length; i++){
            if (element == array[i]){
                return i;
            }
        }
        return -1;
    }

    /**
     * 二分法查找:
     *  1. 二分法查找是建立在已经排好序的数据上的。
     *  2. 原理
     *  10 23 56 100 111 222 235 500 600 arr数组
     *  目标:找出600的下标
     *  (0 + 9) / 2 ---> 4(中间元素的下标)
     *  arr[4] = 100;  100 < 600; 说明被查找元素的位置在右边。
     *  那么此时开始下标变成:4 + 1
     *  (5 +9) / 2 ---> 7(中间元素的下标)
     *  arr[7] = 235; 235 < 600; 说明被查找元素的位置在右边。
     *  开始下标变成了:7 + 1
     *  (8 + 9) / 2 ---> 8
     *  arr[8] = 500; 500 < 600; 说明被查找元素的位置在右边。
     *  开始下标变成了:8 + 1
     *  (9 + 9) /2 ---> 9
     *  arr[9] = 600; 600 = 600; 此时找到了。
     *  终止条件就是当开始下标大于结束下标的时候,即交叉,表示该元素不在数组中。
     *
     * @param array 传入的数组。
     * @param element 被检索的元素。
     * @return 当找到对应的元素时,返回数组的下标。否则返回 -1.
     */
    public static int binarySearch(int[] array, int element){
        int left = 0, right = array.length - 1, mid = 0;  // array.length是从1开始计数,而数组下标是从0开始,故需要减一。
        while (left <= right){
            mid = (left + right) / 2;
            if (array[mid] < element){ // 元素在数组的右侧。
                left = mid + 1;
            } else if (array[mid] > element){ // 元素在数组的左边。
                right = mid - 1;
            } else if (array[mid] == element){
                return mid; // 查找到了
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        int[] a = {4, 6, 2, 5, 7};
        int index = binarySearch(a, 1);
        System.out.println(index == -1 ? "该元素不存在" : "该元素在数组中的位置为:" + index);

    }
}
10.4 java.util.Arrays

java.util.Arrays工具内中包含了很多SUN公司已经写好了的工具类,在今后的开发中我们直接通过API帮助文档进行查找相应的方法即可,切勿死记硬背。

package com.zh0u.javase.常见算法;

import java.util.Arrays;  // 导入Java工具包

public class UseArrays {
    public static void main(String[] args) {
        int[] a = {4, 2, 6, 2, 1, 0, 2};
        Arrays.sort(a);  // 静态方法直接采用“类名.方法”的形式进行访问。

        for (int v: a){
            System.out.print(v + " ");
        }
        System.out.println();
        int i = Arrays.binarySearch(a, 2);
        System.out.println(i);
    }
}

11、String类

注:垃圾回收器不会回收常量–即不会回收字符串常量。

11.1 字符串的存储原理
package com.zh0u.javase.字符串;

/**
 * 关于Java JDK中内置的一个类:java.lang.String
 * 1. String表示字符串类型,属于引用数据类型,不属于基本数据类型。
 * 2. 在Java中随便使用双引号括起来的都是String对象。例如:"abc", "def"这是两个字符串对象。
 * 3. Java中规定,双引号括起来的字符串是不可变的。也就是说"abc"自出生到最终死亡,都不可变,不能变成"abcd",也不能变成"ab"。
 * 4. 在JDK当中双引号括起来的字符串("abc","def")都是直接存储在“方法区”的“字符串常量池”当中。
 * 为什么SUN公司把字符串存储在一个“字符串常量池”当中呢?因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。
 */
public class StringTest01 {
    public static void main(String[] args) {
        // 这两行代码表示底层创建了 3 个字符串对象,都在字符串常量池当中。
        String s1 = "abcdef";
        String s2 = "abcdef" + "xy";

        // 分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?
        // 在Java中,凡是双引号括起来的都在字符串常量池中有一份。
        // new 对象的时候一定在堆内存当中开辟空间。
        String s3 = new String("xy");
    }
}

image-20211025143712073

package com.zh0u.javase.字符串;

public class User {
    private int id;
    private String username;

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public static void main(String[] args) {
        User user = new User(10001, "张三");
    }
}

image-20211025144538504

package com.zh0u.javase.字符串;


public class StringTest02 {
    public static void main(String[] args) {
        String s1 = "hello";
        // "hello" 是存储在方法区的“字符串常量池”当中。
        // 所以这个"hello"是不会新建的,因为这个对象在“字符串常量池”中已经存在了。
        String s2 = "hello";

        // 分析以下结果是 true 还是 false?
        // == 双等号比较的是变量中保存的内存地址。
        System.out.println(s1 == s2);  // true

        String x = new String("xyz");
        String y = new String("xyz");

        // 分析以下结果是 true 还是 false?
        // == 双等号比较的是变量中保存的内存地址。
        System.out.println(x == y); // false

        // 通过这个案例的学习,我们知道了,字符串对象之间的比较不能使用“==”,使用“==”是不安全的。应该调用String类中的equals方法。
        // String类已经重写了equals方法,以下equals方法调用的是String重写之后的equals方法。
        System.out.println(x.equals(y));  // true

        // 以下内容作为补充
        String k = new String("test");
        // String k = null;
        // 为什么 "test" 这个字符串可以后面加 "." 呢?
        // 因为 "test"是一个String字符串对象。只要是都想都能调用方法。
        System.out.println("testString".equals(k));  // 字符串比较的时候建议使用这种方法,可以避免空指针异常。
        System.out.println(k.equals("testString"));  // 存在空指针异常的风险。
    }
}

image-20211025150630193

// 再次强调
int i = 1000; // i变量中保存的是 1000 这个值。

// s变量中保存的是字符串对象的内存地址。s引用中保存的不是"abc",而是"abc"字符串对象在“字符串常量池”当中的内存地址0x1111.
String s = "zh0u9527_";
11.2 String类的构造方法

byte数组传入字符串,将整数经过Stringcode的解码方法转成ASCII编码内容。

package com.zh0u.javase.字符串;


/**
 * 创建字符串的几种 - 构造方法
 * 第一个:String s = " "; 最常用
 * 第二个:String s = new String("");
 * 第三个:String s = new String(char数组);
 * 第四个:String s = new String(char数组, 起始下标, 长度);
 * 第五个:String s = new String(byte数组);
 * 第六个:String s = new String(byte数组, 起始下标, 长度);
 */
public class StringTest04 {
    public static void main(String[] args) {
        // 创建字符串对象最常用的一种方式。
        String s1 = "hello world!";

        // 这里只掌握常用的构造方法。
        byte[] bytes = {97, 98, 99};  // 97 --> a, 98 --> b, 99 --> c
        String s2 = new String(bytes); // 这里可以自动将bytes数组中的数字转换为字符。

        // 前面说过:输出一个引用的时候,会自动调用toString()方法,默认Object的方法会自动输出对象的内存地址。
        // 通过输出结果我们得到一个结论:String类已经重写了toString()方法。
        // 输出字符串对象,输出的不是对象的内存地址,而是字符串本身。
        System.out.println(s2.toString());  // abc
        System.out.println(s2);  // abc

        // String(字节数组, 数组元素下标的起始位置, 截取长度);
        String s3 = new String(bytes, 1, 2);  // 将bytes数组中的一部分转换为“字符串”。注意:offset 计算的下标从零开始。
        System.out.println(s3); // bc

        char[] chars = {'我', '是', '中', '国', '人'};
        // 将char数组全部转换成字符串
        String s4 = new String(chars);
        System.out.println(s4);  // 我是中国人
        // 将char数组部分转换成字符串
        String s5 = new String(chars, 2, 3);
        System.out.println(s5); // 中国人

        // 还有一种
        System.out.println(new String("hello javaEE"));
        
        // idea shift + f12 页面最大化
    }
}
11.3 String中常用的方法
package com.zh0u.javase.字符串;

/**
 * String类当中常用的方法。
 */
public class StringTest05 {
    public static void main(String[] args) {
        // (掌握)1. char charAt(int index);  // 返回指定索引处的 char 值。索引下标从零开始.
        System.out.println("中国人".charAt(1));  //国

        // (了解)2. int compareTo(String anotherString)
        // 字符串之间比较大小不能直接使用 > <符号,需要使用compareTo方法。
        System.out.println("abc".compareTo("abc"));  // 0 前后一致
        System.out.println("abcd".compareTo("abce")); // -1 前小后大
        System.out.println("abce".compareTo("abcd")); // 1 前大后小

        // 如果字符串的第一个字符能比较出结果,后面的字符就不会在比较了。
        System.out.println("xyz".compareTo("yxz")); // -1

        // (掌握)3. boolean contains(CharSequence s)
        // 判断前面的字符串中是否包含后面的子字符串。
        System.out.println("http://www.baidu.com".contains("http"));  // true
        System.out.println("http://www.baidu.com".contains("https://")); // false

        // (掌握)4. boolean endWith(String suffix)
        // 判断当前字符串是否以某个字符串结尾。
        System.out.println("test.txt".endsWith(".java"));  // false
        System.out.println("test.txt".endsWith(".txt"));   // true

        // (掌握)5. boolean equals(Object anObject)
        // 比较两个字符串必须使用equals方法,不能使用“==”。
        // equals 只能看出是否相等。compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。
        System.out.println("java".equals("php"));

        // (掌握)6. boolean equalsIgnoreCase(String anotherString)
        // 比较两个字符串同时忽略字母大小写。
        System.out.println("aBc".equalsIgnoreCase("ABc"));  // true

        // (掌握)7. byte[] getBytes()
        byte[] bytes = "java".getBytes();
        for (byte b:bytes){
            System.out.print(b + " ");  // 106 97 118 97
        }
        System.out.println();

        // (掌握)8. int indexOf(String str)
        // 判断某个子字符串在当前字符串中第一次出现处的索引(下标)。
        System.out.println("oraclec++javaphppython".indexOf("java"));  // 9

        // (掌握)9. boolean isEmpty()
        // 判断某个字符串是否为空,当且仅当 length() 为 0 时返回 true。
        // 注意这里不是 null ,应该注意区分。如果是 null 会出现空指针异常。
        System.out.println("".isEmpty()); // true

        // (掌握)10. int length()
        // 面试题:判断数组长度和判断字符串长度不一样。
        // 判断数组长度是 length 属性,判断字符串长度是 length() 方法。
        System.out.println("abc".length());  // 3

        // (掌握)11. int lastIndexOf(int ch)
        System.out.println("javapythonc++c#javaoracle".lastIndexOf("java"));  // 15

        // (掌握)12. String replace(CharSequence target,CharSequence replacement)
        System.out.println("http://www.baidu.com".replace("http://", "https://"));  // https://www.baidu.com
        // 把以下字符串中的等号 =号全部替换为冒号:
        System.out.println("username=zhangsan&password=zhangsan123&age=20".replace("=", ":")); //username:zhangsan&password:zhangsan123&age:20

        // (掌握)13. String[] split(String regex)
        // 字符串分割,返回一个字符串数组。
        for (String s : "1998/09/06".split("/")) {
            System.out.print(s + " ");  // 1998 09 06
        }
        System.out.println();

        // (掌握)14. boolean startsWith(String prefix)
        System.out.println("http://zh0u-love-study.com".startsWith("http")); // true
        System.out.println("http://zh0u-love-study.com".startsWith("https")); // false

        // (掌握)15. String substring(int beginIndex)
        // 字符串截取,索引下标从零开始
        System.out.println("https://www.souhu.com".substring(8));  // www.baidu.com

        //(掌握)16. String substring(int beginIndex,int endIndex)
        // 从 beginIndex处开始(包括),到 endIndex处结束(包括),即-左闭右开。
        System.out.println("https://www.souhu.com".substring(8, 11));  // www

        // (掌握)17. char[] toCharArray()
        // 将字符串转换为 char 数组。
        for (char c : "我是中国人".toCharArray()) {
            System.out.print(c + " "); // 我 是 中 国 人
        }
        System.out.println();

        // (掌握)18. String toLowerCase() , String toUpperCase()
        System.out.println("JAVa".toLowerCase());  // java
        System.out.println("javA".toUpperCase());  // JAVA

        // (掌握)19. String trim()
        // 去除字符串前后空白。
        System.out.println("          hello       world              ".trim());  //hello       world

        // (掌握)20. static String valueOf(boolean b)
        // String中的一个方法是静态方法,不需要new对象。作用:将“非字符串”转换为“字符串”。
        System.out.println(String.valueOf(true));  // true
        System.out.println(String.valueOf(3).length());  // 1

        // 这个静态的valueOf()方法,参数是一个对象的时候,会自动调用该对象的toString()方法。
        System.out.println(String.valueOf(new Customer())); // Customer{}

        // 研究一下println()方法源代码。
        System.out.println(100); // 100
        // 通过源代码可以看出,为什么输出一个引用的时候会自动调用toString()方法。
        // 本质上System.out.println()这个方法在输出任何数据的时候都是先转换成字符串,再输出。
        System.out.println(new Customer());  //  Customer{}
    }
}

class Customer{
    // 重写toString()方法

    @Override
    public String toString() {
        return "Customer{}";
    }
}

12、StringBuffer

package com.zh0u.javase.stringBuffer;

/**
 * 思考:在实际开发中,如果需要进行字符串的频繁拼接,会有什么问题?
 *  因为Java中的字符串是不可变的,每一次片接都会产生新字符串。
 *  这样回占用方法区大量的内存。造成内存空间的浪费。
 *      String s = "abc";
 *      s += "hello";
 *      就以上两行代码,就导致在方法区字符串常量池当中创建了3个对象。
 *          "abc"
 *          "hello"
 *          "abchello"
 */
public class StringBufferTest01 {
    public static void main(String[] args) {
        String s = "";
        // 以下代码会给Java的方法区字符串常量池带来很大的压力。
        for (int i = 0; i < 100; i++) {
            s = s + i;
            System.out.println(s);
        }
    }
}
12.1 StringBuffer的原理
package com.zh0u.javase.stringBuffer;

import com.sun.org.apache.xpath.internal.operations.String;

/**
 * 如果以后需要进行大量字符串的拼接操作,建议使用JDK中自带的:
 *      java.lang.StringBuffer
 *      java.lang.StringBuilder
 *
 * 如何优化StringBuffer的性能?
 *      在创建StringBuffer的时候尽可能给定一个初始化容量。
 *      最好减少底层数组的扩容次数。预估计一下,给一个大一些初始化的容量。
 *
 *      关键点:给一个合适的初始化容量。可以提高程序的执行效率。
 */
public class StringBufferTest02 {
    public static void main(String[] args) {
        // 创建一个初始化容量为 16 的 byte[] 数组。(字符串缓冲区对象)
        StringBuffer stringBuffer = new StringBuffer();

        // 拼接字符串,以后拼接字符串统一调用 append() 方法。
        // append()方法底层在进行追加的时候,如果 byte[] 数组满了,会自动扩容。
        stringBuffer.append('a');

        // 指定初始化容量的 StringBuffer 对象(字符串缓冲区对象)。
        StringBuffer buffer = new StringBuffer(100);
        buffer.append("hello");
        buffer.append("world");

        System.out.println(buffer);  // 会自动调用 toString() 方法。
    }
}
12.2 StringBuffer与StringBuilder的区别
package com.zh0u.javase.stringBuffer;

/**
 * java.lang.StringBuilder
 * StringBuffer和StringBuilder的区别?
 *      StringBuffer中的方法都有:synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。
 *      StringBuilder中的方法都没有:synchronized关键字修饰。表示StringBuilder在多线程环境下运行是不安全的。
 *
 *      StringBuffer是线程安全的。
 *      StringBuilder是非线程安全的。
 */
public class StringBuilderTest01 {
    public static void main(String[] args) {
        // 使用StringBuilder也可以完成字符串的拼接。
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(100);
        stringBuilder.append("hello");

        System.out.println(stringBuilder); // 100hello
    }
}
12.3 面试题补充
package com.zh0u.javase.字符串;

/**
 * 面试题:
 *  1. String为什么不可变?
 *      我看过源代码,String类中有一个byte[]数组,这个byte[]数组采用了final修饰,因为数组一旦创建长度不可变。
 *      并且被final修饰的引用一旦指向某个对象之后,不可在指向其他对象,所以String不可变!
 *          "abc"无法变成"abcd";
 *
 *  2. StringBuffer/StringBuilder为什么可变?
 *     我看过源代码,StringBuffer/StringBuilder内部实际上是一个byte[]数组,这个byte[]数组没有被final修饰,StringBuffer/StringBuilder的初始化
 *     容量应该是16,当存满之后会进行扩容,底层调用了数组拷贝的方法System.arraycopy()...。StringBuffer/StringBuilder一般用于字符串的频繁拼接。
 */
public class StringTest06 {
    public static void main(String[] args) {
        // 字符串不可变是什么意思?
        // 是说双引号里面的字符串对象一旦创建不可变。
        String s1 = "abc";  // "abc"放到了字符串常量池当中。"abc"不可变。

        // s1变量可以指向其它对象的地址。
        // 字符串不可变不是说以上变量s不可变。说的是"abc"这个对象不可变。
        s1 = "def";  // "def"放到了字符串常量池当中。"def"不可变。
    }
}

13、包装类

13.1 包装类存在的意义

myInt.java

package com.zh0u.javase.integer;

public class myInt {
    private int value;

    public myInt(int value) {
        this.value = value;
    }
    
    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

IntegerTest01.java

package com.zh0u.javase.integer;

/**
 * 1. java中为8中基本数据类型又对应准备了8种包装类型,8中包装类属于引用数据类型,父类是Object。
 * 2. 思考:为什么要提供8中包装类呢?因为8中基本数据类型不够用,所以SUN公司又提供对应的8种包装类型。
 */
public class IntegerTest01 {


    // 入口
    public static void main(String[] args) {
        // 有没有这种需求:调用doSome()方法的时候需要传一个数字进去。
        // 但是数字属于基本数据类型,而doSome()方法参数的类型是Object。
        // 可见doSome()方法无法接收基本数据类型的数字。那怎么办呢?可以传一个数字对应的包装类进去。

        // 把 100 这个数字经过构造方法包装成对象。
        myInt myInt = new myInt(100);
        doSome(myInt);
    }

    public static void doSome(Object obj){
        System.out.println(obj);
    }
}
13.2 八中包装类
package com.zh0u.javase.integer;

/**
 * 1. 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
 *
 * 2. 以上八中包装类中,重点以java.lang.Integer为代表进行学习,其它的类型照葫芦画瓢就行了。
 */

public class InterTest02 {
}
13.3 装箱与拆箱
package com.zh0u.javase.integer;

/**
 *    Number是一个抽象类,无法实例化对象。
 *    Number类中有这样的方法:
 *      byte byteValue() 以 byte 形式返回指定的数值。
 *      abstract  double doubleValue()以 double 形式返回指定的数值。
 *      abstract  float floatValue()以 float 形式返回指定的数值。
 *      abstract  int intValue()以 int 形式返回指定的数值。
 *      abstract  long longValue()以 long 形式返回指定的数值。
 *      short shortValue()以 short 形式返回指定的数值。
 *   这些方法全部是拆箱的方法。
 */

public class InterTest02 {
    public static void main(String[] args) {
        // 123这个基本数据类型进行构造方法的包装达到了:基本数据类型性引用数据类型的转换。
        // 基本数据类型 -> 引用数据类型(装箱)
        Integer integer = new Integer(100);
        System.out.println(integer.intValue());  // 100

        // 引用数据类型 --> 基本数据类型(拆箱)
        System.out.println(integer.floatValue()); // 100.0
    }
}
13.4 Integer构造方法
package com.zh0u.javase.integer;

/*
    关于Integer类的构造方法,有两个:
        Integer(int);
        Integer(String);
 */
public class IntegerTest03 {
    public static void main(String[] args) {
        // Java9之后不建议使用这个构造方法。
        // 将数字100转换成Integer包装类型(int --> Integer)
        Integer i1 = new Integer(100);
        System.out.println(i1);

        // 将字符串1000转换成Integer包装类型(String --> Integer)
        Integer i2 = new Integer("1000");
        System.out.println(i2.toString());
    }
}
13.5 最大值与最小值
package com.zh0u.javase.integer;

/**
 * 最大值与最小值的问题。
 */
public class IntegerTest04 {
    public static void main(String[] args) {
        // 通过访问包装类的常量,来获取最大值和最小值。
        System.out.println("int 的最大值 : " + Integer.MAX_VALUE);  // 2147483647
        System.out.println("int 的最小值 : " + Integer.MIN_VALUE);  // -2147483648
        System.out.println("byte 的最大值 : " + Byte.MAX_VALUE);  // 127
        System.out.println("byte 的最小值 : " + Byte.MIN_VALUE);  // -128
    }
}
13.6 自动装箱与自动拆箱
package com.zh0u.javase.integer;

/**
 * 好消息:在Java5之后,引用一种新特性,自动装箱和自动拆箱。
 *      自动装箱:基本数据类型自动转换成包装类型。
 *      自动拆箱:包装类型自动转换成基本数据类型。
 *
 * 有了自动拆箱之后,Number类中的方法就用不着了!
 */
public class IntegerTest05 {
    public static void main(String[] args) {
        // 900 是基本数据类型
        // x 是包装类型
        // 基本数据类型 --(自动转换)--> 包装类型:自动装箱。
        Integer x = 900;
        System.out.println(x.toString()); // 900

        // x 是包装类型
        // y 是基本数据类型
        // 包装类型 --(自动转换)--> 基本数据类型:自动拆箱
        int y = x;
        System.out.println(y);

        // z 是一个引用变量,保存的还是一个对象的内存地址。
        Integer z = 1000; // 等同于: Integer z = new Integer(1000);
        // 分析以下代码为什么没有报错?
        // + 号两边要求是基本数类型的数字,z是包装类型,不属于基本数据类型,这里会进行自动拆箱。将z转换成基本数据类型。
        // 以这一行代码在java5之前编译器会报错。
        System.out.println(z + 1);

        Integer a = 1000;
        Integer b = 1000;
        // == 比较的是对象的内存地址,a和b两个引用中保存的对象内存地址不同。
        // == 这个运算符不会触发自动拆箱机制。(只有 + - * / 等运算符的时候才会触发)
        System.out.println(a == b);  // false
    }
}

image-20211026153121669

13.7 重要面试题补充
package com.zh0u.javase.integer;

/*
    分析以下程序为什么?

    这是Integer非常重要的一个面试题目。
 */
public class IntegerTest06 {
    public static void main(String[] args) {
        Integer a = 128;
        Integer b = 128;
        System.out.println(a == b); // false

        /*
            java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,
            放到了一个方法区的“整数型常量池”当中了,目的是只要用这个区间的数据就不需要在new了,直接从整数型常量池当中取出来。

            原理:x变量中保存的对象的内存地址和y变量中保存的对象的内存内存地址是一样的。
         */
        Integer x = 127;
        Integer y = 127;
        // 记住:== 永远比较的是对象的内存地址。
        System.out.println(x == y);  // true
    }
}

image-20211026154702857

13.8 Integer常用方法
package com.zh0u.javase.integer;

// Integer中常用的方法。

// 常见异常:
// 空指针异常 ---> NullPointException
// 类型转换异常 ---> ClassCastException
// 数组下标越界异常 ---> ArrayIndexOutOfBoundsException
// 数字格式化异常 ---> NumberFormatException
public class IntegerTest07 {
    public static void main(String[] args) {
        // 手动装箱
        Integer a = new Integer(100);

        // 手动拆箱
        // (掌握)1. intValue()
        int b = a.intValue();
        System.out.println(b); // 100

        Integer c = new Integer("123");
        System.out.println(c.intValue());  // 123

        // 分析以下代码
        // 编译的时候没有问题,一切符合java语法,运行的时候会不会出现问题?不是一个“数字”可以包装成Integer嘛?答案:不能
        // 运行时出现异常:java.lang.NumberFormatException
        // Integer a = new Integer("中文");

        // 重点方法
        // static int parseInt(String s)throws NumberFormatException
        // 静态方法,传参String,返回int。 String ---> int
        // 注:如果这里的字符串不是数字字符串则会抛出异常 : NumberFormatException
        System.out.println(Integer.parseInt("123") + 100);  // 223

        // ---------------以下方法作为了解-------------------
        // 1. static String toBinaryString(int i)
        System.out.println(Integer.toBinaryString(3)); // 二进制数 : 11

        // static String toHexString(int i)
        // 十六进制 : 0 1 2 3 4 5 6 7 8 9 a b c d e f
        System.out.println(Integer.toHexString(10));  // a

        // static String toOctalString(int i)
        // 八进制 : 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16
        // 十进制 : 0 1 2 3 4 5 6 7 8  9  10 11 12 13 14
        System.out.println(Integer.toOctalString(14));  // 16

        // static Integer valueOf(int i)
        // 将 int ---> Integer
        // 将 int 100 装换为 Integer 100
        System.out.println(Integer.valueOf(100));  // 100
    }
}
13.9 String Integer int 三种的互相转换
package com.zh0u.javase.integer;

/**
 * String int Integer之间互相转换
 */
public class IntegerTest08 {
    public static void main(String[] args) {

        // String --> int
        int a1 = Integer.parseInt("100");  // 将字符串转换为int
        System.out.println(a1 + 1); // 101
        
        // int --> String
        // 方式一:
        String s2 = a1 + "";  // "100"字符串
        System.out.println(s2 + 1); // "1001"字符串
        
        // 方式二:
        String s1 = String.valueOf(123);  // 将int转换为字符串
        System.out.println(s1); // 123

        // int --> Integer (建议使用自动装箱)

        // 使用自动装箱
        Integer i10 = 1000;

        // 非自动装箱
        int a2 = 3;  // 创建int变量 a2
        Integer i1 = Integer.valueOf(a2); // 将int变量a2转换为Integer对象
        System.out.println(i1); // 3

        // Integer --> int(建议使用自动拆箱)
        // 自动拆箱
        int i11 = i10;

        // 非自动装箱
        Integer i2 = new Integer(200);  // 创建Integer对象
        int a3 = i2.intValue(); // 将Integer对象转换为int
        System.out.println(a3); // 200

        // String --> Integer
        Integer i3 = Integer.valueOf("1000");  // 将字符串1000解析为Integer对象并赋值给i3
        System.out.println(i3);  // 1000
        
        // Integer --> String
        String s3 = String.valueOf(new Integer(225)); // 将Integer对象解析为String对象并赋值给s2
        System.out.println(s3);  // 225
    }
}

image-20211026172132796

14、Date类

14.1 获取当前日期并格式化
package com.zh0u.javase.date;

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

/*
java 中对日期的处理
    这个案例最主要掌握:
        知识点1:怎么获取系统当前时间
        知识点2:String-->Date
        知识点3:Date-->String
 */
public class DateTest01 {
    public static void main(String[] args) throws ParseException {
        // 获取系统当前时间(精确到毫秒的系统当前时间)
        // 直接调用无参数构造方法就可以。
        Date nowDate = new Date();

        // java.lang.Date类的toString()方法已经被重写了。
        // 输出的应该不是一个对象的内存地址,应该是一个日期字符串。
        // System.out.println(nowDate); // Tue Oct 26 19:09:46 CST 2021

        // 日期的格式化
        // 将日期类型Date,按照指定的格式进行转换:Date -- 转换成具有一定格式的日期字符串-->String
        // SimpleDateFormat是Java.text包下的。专门负责日期格式化的。
        /*
        以下的字母表示中,有几个字母就表示有几位数字。
        yyyy 年 2021
        MM 月 10
        dd 日 26
        HH 时 20
        mm 分 21
        ss 秒 24
        SSS 毫秒(三位数,最高999。1000代表一秒)
         */
        // 将日期格式设置为:年月日 时分秒 。 如:2021-10-26 19:24:30
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(sdf.format(nowDate));  // 2021-10-26 19:24:54

        // 假设现在有一个日期字符串String,怎么转换成Date类型。
        // String ---> Date
        String time = "2021/10/26 19:32:20";
        // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("格式不能乱序,必须要和日期字符串格式相同");
        // 注意:字符串的日期格式和SimpleDateFormat对象指定的日期格式要一致。不然会出现异常:java.text.ParseException
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date dateTime = sdf2.parse(time);
        System.out.println(dateTime);  // Tue Oct 26 19:32:20 CST 2021
    }
}
14.2 计算一个方法执行的时间
package com.zh0u.javase.date;

/*
获取自1970年1月1日 00:00:00到当前系统时间的总毫秒数。
1秒 = 1000毫秒。

简单总结一下System类的相关属性和方法:
    System.out [out是System类的静态变量]
    System.out.println() [println()方法不是System类的,是PrintStream类的方法]
    System.gc() 建议启动垃圾回收器
    System.currentTimeMillis() 获取自1970年1月1日到系统当前时间的总毫秒数。
    System.exit(0) 退出JVM。
 */
public class DateTest02 {
    public static void main(String[] args) {
        // 获取自1970年1月1日 00:00:00到当前系统时间的总毫秒数。
        long nowTimeMillis = System.currentTimeMillis();
        System.out.println(nowTimeMillis);  // 1635248651231

        // System.currentTimeMillis();这个方法的返回值有什么用?
        // 我们可以用来查看一个方法执行所耗费的时间。
        // 在调用目标方法之前记录一个毫秒数
        long start = System.currentTimeMillis();
        // 执行方法
        printI();
        // 在调用目标方法之后记录一个毫秒数
        long end = System.currentTimeMillis();
        System.out.println("printI()方法执行所耗费的时长为 : " + (end - start) + "毫秒");

    }

    public static void printI(){
        for (int i = 0; i < 10000; i++){
            // 打印输出到屏幕会严重影响代码执行的速度,可以将其注释后在执行,速度会有很大的变化。 没有输出语句:0毫秒
//            System.out.println(i);  // 75毫秒
        }
    }
}
14.3 毫秒构造Date日期
package com.zh0u.javase.date;

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

public class DateTest03 {
    public static void main(String[] args) {
        // 这个时间是1970年1月1日 00:00:00自今总的毫秒数
        Date time = new Date(1);  // 这里的参数表示的是1毫秒

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        String strTime = sdf.format(time);
        // 北京是东8区。差8个小时。
        System.out.println(strTime);  // 1970-01-01 08:00:00 001

        // 获取昨天此时的时间。1000 * 60 * 60 * 24 一天的毫秒数。
        Date time2 = new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24);
        String strTime2 = sdf.format(time2);
        System.out.println(strTime2);  // 2021-10-25 20:13:50 309
    }
}

15、数字类(了解)

15.1 数字格式化
package com.zh0u.javase.number;

import java.text.DecimalFormat;

/*
关于数字的格式化。(了解)
 */
public class DecimalFormatTest01 {
    public static void main(String[] args) {
        // DecimalFormat 专门用来做数字的格式化
        // DecimalFormat df = new DecimalFormat("数字格式");

        /*
        数字格式有哪些?
            # 代表任意数字
            , 代表千分位
            . 代表小数点
            0 代表不够时补0

            ###,###.##
                表示:加入千分位,保留2个小数。
         */
        DecimalFormat df = new DecimalFormat("###,###.##");
        String s = df.format(1234556.23);
        System.out.println(s); // 1,234,556.23

        DecimalFormat df2 = new DecimalFormat("###,###.0000"); // 保留4个小数位,不够补上0
        String s2 = df2.format(1234.56);
        System.out.println(s2);  // 1,234.5600
    }
}
15.2 高精度BigDecimal
package com.zh0u.javase.number;

import java.math.BigDecimal;

/**
 * 1. BigDecimal 属于大数据,精度极高。不属于基本数据类型,属于java对象(引用数据类型)。
 * 这是SUN公司提供的一个类。专门用在财务软件当中。
 *
 * 2. 注意:财务软件中double是不够的,应该使用java.math.BigDecimal
 */
public class BigDecimalTest01 {
    public static void main(String[] args) {
        // 这个100不是普通的100,是精度极高的100
        BigDecimal v1 = new BigDecimal(100);
        // 精度极高的200
        BigDecimal v2 = new BigDecimal(200);
        // 求和
        // v1 + v2; // 这样不行,v1和v2都是引用,不能直接使用 + 号来求和。
        BigDecimal v3 = v1.add(v2);
        System.out.println(v3); // 300
    }
}

16、随机数

package com.zh0u.javase.random;

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

/**
 * 随机数
 */
public class RandomTest01 {
    public static void main(String[] args) {
        // 创建随机数对象
        Random random = new Random();

        // 随机产生一个int类型取值范围内的数字
        int anInt = random.nextInt();
        // System.out.println(anInt);

        // 产生[0~100]之间的随机数。
        int anInt1 = random.nextInt(101); // 不包括 101
        // System.out.println(anInt1);

        arrayRandomValue();
    }

    /**
     * 编写程序,生成5个不重复的随机数。重复的话重新生成。
     * 最终生成的5个随机数放到数组中,要求数组中这5个随机数不重复。
     */
    public static void arrayRandomValue(){
        int[] array = new int[5];  // 默认值都是 0
        Random random = new Random();
        Arrays.fill(array, -1);
        int index = 0; // 下标
        while (index < array.length){
            int num = random.nextInt(101);
            // 判断array数组中有没有当前这个num,如果没有,就放进去。
            if (!inArray(array, num)){
                array[index++] = num;
            }
        }
        for (int v: array){
            System.out.print(v + " ");
        }
    }

    /**
     * 判断数组中是否包含某个元素
     * @param array 传入的数组
     * @param key 要检查的元素
     * @return true表示存在,false表示不存在。
     */
    public static boolean inArray(int[] array, int key){
        /*
        这里的这个方法存在 bug,还是一个一个一一对比。
        Arrays.sort(array);  // 首先排序
        // binarySearch()方法找到元素返回>=0的值。
        return Arrays.binarySearch(array, key) >= 0;
        */
        // 使用以下方法不存在bug,只是数独有点慢
        for (int v: array){
            if (v == key){
                return true;
            }
        }
        return false;
    }
}

17、ENUM枚举

为什么使用枚举?目的是解决多种特定已知值的情况。

package com.zh0u.javase.enumerate;

/*
枚举类型
总结:
    1. 枚举是一种引用数据类型。
    2. 枚举类型怎么定义?语法是?
        enum 枚举类型名{
            枚举值1,枚举值2
        }
    3. 结果只有两种情况的,建议使用boolean类型。
    结果超过两种并且还是可以一枚一枚列举出来的,建议使用枚举类型。
        例如:颜色、四季、星期等都可以使用枚举类型。
*/
public class EnumTest01 {
    public static void main(String[] args) {
        System.out.println(divideByBool(2, 1) ? "计算成功" : "计算失败");
        // 使用枚举,返回值也是枚举类型
        Result result = divideByEnum(1, 1);
        System.out.println(result == Result.SUCCESS ? "计算成功" : "计算失败");
    }

    /**
     * 计算两种两个整数相除是否成功
     * @param a 被除数
     * @param b 除数
     * @return 成功返回true,失败返回false
     */
    // 如果返回值只有两种类型建议使用boolean类型。
    public static boolean divideByBool(int a, int b){
        try{
            int c = a / b;
            return true;
        } catch (Exception e){
            return false;
        }
    }

    // 如果这里的返回值类型大于两种情况建议使用enum枚举类型。
    public static Result divideByEnum(int a, int b){
        try{
            int c = a / b;
            return Result.SUCCESS;
        } catch (Exception e){
            return Result.FAIL;
        }
    }

}

// 枚举:一枚一枚可以列举出来的,才建议使用枚举类型。
// 枚举编译之后也是生成class文件,枚举是一种引用数据类型,其中的每一个值可以看做是常量。
enum Result{
    // SUCCESS 是枚举Result类型中的一个值
    // FAIL 是枚举Result类型中的一个值
    // 枚举中的每一个值都可以看做是“常量”
    SUCCESS, FAIL
}

18、异常处理

18.1 什么是异常
package com.zh0u.exception;

/*
1. 什么是异常?Java提供异常处理机制有什么用?
    程序执行过程中发生了不正常的情况,而这种不正常的情况叫做“异常”。java语言是很完善的语言,提供了异常的处理方式,以下程序执行过程中出现了不正常的情况。
    java把该异常信息打印输出到控制台,拱程序员参考。这样可以让程序变得更加健壮。
2. 异常输出
    int a = 10;
    int b = 0;
    int c = a / b;
    System.out.println(a + "/" + b + "=" + c);
    控制台输出:
        Exception in thread "main" java.lang.ArithmeticException: / by zero
	        at com.zh0u.exception.ExceptionTest01.main(ExceptionTest01.java:12)
	这段异常信息是由Java虚拟机JVM打印的。
*/
public class ExceptionTest01 {
    public static void main(String[] args) {
        /*
        int a = 10;
        int b = 0;
        int c = a / b;
        System.out.println(a + "/" + b + "=" + c);
        */
        // 改善
        int a = 10;
        int b = 1;
        if (b == 0){
            System.out.println("除数不能为零");
            return;
        }
        int c = a / b;
        System.out.println(a + "/" + b + "=" + c);
    }
}
18.2 异常的存在形式
package com.zh0u.exception;

/*
   java语言异常是以什么形式存在的呢?
        1. 异常在java中以类的形式存在,每一个异常类都可以创建异常对象。
        2. 异常对应的显示生活中是怎样的?
            火灾(异常类):
                2008年9月1日,小明家着火了(异常对象)
                2008年10月1日,小花家着火了(异常对象)
                2008年11月1日,小刚家着火了(异常对象)
            类的本质是“模板”
            对象的本质是实际存在的个体。
*/
public class ExceptionTest02 {
    public static void main(String[] args) {
        Exception exception = new ArithmeticException("算术异常");
        System.out.println(exception); // 算术异常
        int x = 1, y = 0,c;

        // 实际上JVM在执行到此处的时候,会new一个异常对象:new ArithmeticException("/ by zero");然后将这个异常抛出。
        // 并且JVM将new的异常对象抛出,打印输出信息到控制台。
        c = x / y;
        System.out.println(c);

        // 程序运行到这里也会new出一个ArithmeticException异常,这个异常以上面的异常不是同一个。
        // System.out.println(100 / 0);
    }0
}
18.3 java的异常处理机制

1.1、异常在java中以类和对象的形式存在。那么异常的继承结构是怎样的?我们可以使用UML图来描述一下继承结构。画UML图有很多工具,例如:Rational Rose(收费),starUML等……

Object
Object下有Throwable(可抛出的)的子类。
Throwable下有两个分支:Error(不可处理,直接退出JVM)和Exception(可处理的)。
Exception下有两个分支:
	Exception的直接子类:编译时异常(要求程序员在编写程序阶段必须预先对这些异常进行处理,因此得名编译是异常)
	RunTimeException:运行时异常(在编写程序阶段程序员可以预先处理,也可以不管)。

1.2、编译时异常和运行时异常,都是发生在运行阶段。编译阶段一场是不会发生的,编译时异常因为什么而得名?

​ 因为变时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,因此得名。所有的异常都是在程序运行阶段发生的。因为只有程序运行阶段才可以new对象。因为异常的发生就是new对象。

1.3、编译时异常和运行时异常的区别?

​ 编译时异常一般发生的概率比较高

​ 对于一些发生概率较高的异常,需要在运行之前对其进行预处理。

​ 运行时异常一般发生的概率比较低。

​ 例如:小明走在大街上,可能会被天上的飞机轮子砸到,被飞机轮子砸到也算是一种异常,但是这种异常发生的概率比较低。在出门之前你没必要提前对这种发生概率较低的异常进行预处理,如果你预处理这种异常,你将活得很累。

假设Java中没有对异常进行划分,没有分为:编译是异常和运行时异常,所有的异常都需要在编写程序阶段对其进行预处理,将是怎样的效果?如果是这样的话,程序肯定是绝对的安全,但是程序员编写程序太累,代码导出都是在处理异常。

image-20211028200358250

1.4、千万要记住,所有的异常都是发生在运行阶段。

18.4 、Java语言中对异常的处理包括两种方式:

​ 第一种:在方法声明的位置上使用throws关键字,抛给上一级,谁调用我,我就抛给谁。

​ 第二种:使用try…catch语句进行异常的捕捉。

​ 思考:异常发生之后,如果我选择了上抛,抛给了我的调用者,调用者需要对这个异常继续处理,那么调用者处理这个异常同样有两种处理方式。

1.8、注意:Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果,终止java程序的运行。

package com.zh0u.exception;

public class ExceptionTest03 {
    public static void main(String[] args) {
        /*
            这里是main方法区调用的println()方法,这个异常出现后,main方法没有处理。
            而是将它抛给JVM虚拟机,虚拟机终止程序执行。
            ArithmeticException 异常属于运行时异常,继承至 RuntimeException。
        */
        System.out.println(100 / 0);

        // 这里的hello world并不会输出,原因是代码没有被执行。
        System.out.println("hello world!");
    }
}
package com.zh0u.exception;

/*
以下代码报错的云因是什么?
    因为doSome()方法声明位置上使用了:throws ClassNotFoundException
    而ClassNotFoundException是编译时异常。必须在编写代码时处理,没有处理编译器报错。
*/
public class ExceptionTest04 {
    public static void main(String[] args) {
        /*
        main方法值调用doSome()方法。
        因为doSome()方法声明位置上有:throws ClassNotFoundException
        我们在调用doSome()方法的时候必须对这种异常进行预先的处理。
        如果不处理,编译器报错,报错信息:Unhandled exception: java.lang.ClassNotFoundException
        */
        doSome();
    }
    /**
     * doSome方法在方法声明的位置上使用了:throws ClassNotFoundException
     * 这个代码表示doSome()方法在执行过程中,有可能会出现ClassNotFoundException异常。
     * 叫做“类没有找到异常”。这个异常直接父类是:Exception,所以ClassNotFoundException属于编译是异常。
     * @throws ClassNotFoundException
     */
    public static void doSome() throws ClassNotFoundException{
        System.out.println("doSome!!!");
    }
}
package com.zh0u.exception;

public class ExceptionTest05 {
    // 第一种处理方式:在方法声明的位置上继续使用:throws来完成异常的继续上抛。抛给调用者。
    // 上抛类似有推卸责任。(继续把异常传递给调用者。)
    /*
    public static void main(String[] args) throws ClassNotFoundException {
        doSome();
    }
    */
    // 第二种方式:使用try...catch方式捕获
    // 捕捉等于把异常拦下来,异常真正地解决了。(调用者是不知道的)
    public static void main(String[] args) {
        try {
            doSome();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void doSome() throws ClassNotFoundException{
        System.out.println("doSome!!!");
    }
}
18.5 编译时异常
package com.zh0u.exception;

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

/**
 * 处理异常的第一种方式:
 *      在方法声明的位置上使用throws关键字抛出,对调用这个方法,就抛给谁,抛给调用者来处理。
 *      这种处理异常的态度:上报
 *
 * 处理异常的第二种方式:
 *      使用try...catch语句对异常进行捕捉,这个异常不会上报,自己把这个事处理了。
 *      异常抛到此处为止,不在上抛了。
 *      
 * 注意:
 *      只要异常没有捕捉,采用上报的方式,此方法的后续代码不会被执行。
 *      另外需要注意,try语句块中的某一行出现异常,改行后面的代码不会执行。
 *      try...catch捕捉异常之后,后续(try...catch语句块之外的代码)代码可以继续执行
 */
public class ExceptionTest06 {
    // 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。
    // JVM只有终止。异常处理机制的作用就是增强程序的健壮性。这样能做到异常发生率 也不影响程序的继续执行。
    // 一般main方法中的异常建议直接使用try...catch进行捕捉。main方法就不要再往上抛了。
    public static void main(String[] args) {
        System.out.println("main begin!!!");
        // 这里使用try...catch方式进行异常的捕获
        try {
            // 当m1()方法抛出异常时,在try语句代码块中m1()以下的代码便不会执行。
            // 由于这里使用的是try...catch即异常捕捉,所以当m1()方法执行异常,程序直接跳到catch语句中执行响应的代码。
            // 而且等catch语句代码块执行完毕后,继续执行下面的代码块。如:System.out.println("main end!!!");
            m1();
            System.out.println("java-main-method!");
        } catch (FileNotFoundException e) {  // 这里的变量 e 保存的是产生异常时 new 对象的内存地址。
            System.out.println("文件没有找到!!!");
        }
        System.out.println("main end!!!");
    }

    private static void m1() throws FileNotFoundException{
        System.out.println("m1 begin!!!");
        m2();  // 如果m2()方法抛出异常,则m2()方法以下的代码便不会执行了,而且m1()方法继续抛出异常。
        System.out.println("m1 end!!!");
    }

    /**
     * 抛别的不行,抛出ClassCastException说明你还是没有对FileNotFoundException进行处理。
     * private static void m2() throws ClassCastException{}
     * 抛FileNotFoundException的父对象IOException是可以的。因为IOException包括FileNotFoundException。
     * private static void m2() throws IOException{}
     * throws后面也可以写多个异常,使用逗号隔开即可。
     * private static void m2() throws ClassCastException, FileNotFoundException{}
     */
    private static void m2() throws FileNotFoundException {
        System.out.println("m2 begin!!!");
        // 编译器报错的原因是:m3()方法声明位置上有 throws FileNotFoundException
        // 但是我们并没有在调用m3()方法的m2()方法声明的位置上进行预处理,所以编译器报错。
        m3();  // 如果m3抛出异常,则m3()方法以下的代码也不会执行了,并且m2()方法也会抛出异常
        System.out.println("m2 end!!!");
    }

    private static void m3() throws FileNotFoundException {
        /*
         * 调用SUN JDK中某个类的构造方法。
         * 这个类还没有接触过,后期IO流的时候就知道了。
         * 我们只是借助这个类学习下异常处理机制。
         * 创建一个输入流对象,该流指向一个文件。
         *
         * 编译报错的原因是什么?
         *      因为这里的调用的构造方法:FileInputStream(String name)在声明上有 throws FileNotFoundException。
         *      而且这类的继承自:IOException,而IOException继承自Exception属于“编译是异常”。
         * 错误原因:编译是异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就会报错。
         */
        // 我们采用第一种处理方式:在方法声明的位置上使用throws继续上抛。
        // 一个方法体当中的代码出现异常之后,如果上报的话,此方法结束。
        new FileInputStream("C:\\Users\\friendship\\Desktop\\jjjjj.txt");
        // 当上一行代码发生异常时这里的代码便不会执行了
        System.out.println("java-m3-method!");
    }
}
package com.zh0u.exception;

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

/**
 * 处理异常的第一种方式:
 *      在方法声明的位置上使用throws关键字抛出,对调用这个方法,就抛给谁,抛给调用者来处理。
 *      这种处理异常的态度:上报
 *
 * 处理异常的第二种方式:
 *      使用try...catch语句对异常进行捕捉,这个异常不会上报,自己把这个事处理了。
 *      异常抛到此处为止,不在上抛了。
 */
public class ExceptionTest06 {
    // 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。
    // JVM只有终止。异常处理机制的作用就是增强程序的健壮性。这样能做到异常发生率 也不影响程序的继续执行。
    // 一般main方法中的异常建议直接使用try...catch进行捕捉。main方法就不要再往上抛了。
    public static void main(String[] args) {
        System.out.println("main begin!!!");
        // 这里使用try...catch方式进行异常的捕获
        try {
            m1();
        } catch (FileNotFoundException e) {
            System.out.println("文件没有被照到!!!");
        }
        System.out.println("main end!!!");
    }

    private static void m1() throws FileNotFoundException{
        System.out.println("main begin!!!");
        m2();
        System.out.println("main end!!!");
    }

    /**
     * 抛别的不行,抛出ClassCastException说明你还是没有对FileNotFoundException进行处理。
     * private static void m2() throws ClassCastException{}
     * 抛FileNotFoundException的父对象IOException是可以的。因为IOException包括FileNotFoundException。
     * private static void m2() throws IOException{}
     * throws后面也可以写多个异常,使用逗号隔开即可。
     * private static void m2() throws ClassCastException, FileNotFoundException{}
     */
    private static void m2() throws FileNotFoundException {
        System.out.println("main begin!!!");
        // 编译器报错的原因是:m3()方法声明位置上有 throws FileNotFoundException
        // 但是我们并没有在调用m3()方法的m2()方法声明的位置上进行预处理,所以编译器报错。
        m3();
        System.out.println("main end!!!");
    }

    private static void m3() throws FileNotFoundException {
        /*
         * 调用SUN JDK中某个类的构造方法。
         * 这个类还没有接触过,后期IO流的时候就知道了。
         * 我们只是借助这个类学习下异常处理机制。
         * 创建一个输入流对象,该流指向一个文件。
         *
         * 编译报错的原因是什么?
         *      因为这里的调用的构造方法:FileInputStream(String name)在声明上有 throws FileNotFoundException。
         *      而且这类的继承自:IOException,而IOException继承自Exception属于“编译是异常”。
         * 错误原因:编译是异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就会报错。
         */
        // 我们采用第一种处理方式:在方法声明的位置上使用throws继续上抛。
        new FileInputStream("C:\\Users\\friendship\\Desktop\\list.txt");
    }
}
18.6 try…catch的深入
package com.zh0u.exception;

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

/**
 * try...catch的深入
 *      1. catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
 *      2. catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
 *      3. catch写过歌的时候,从上到下,必须遵循从小到大原则,即先子类在父类再父类的父类
 */
public class ExceptionTest07 {
    public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("C:\\Users\\friendship\\Desktop\\jjjj");
            fileInputStream.read();
            // 注意这里catch语句顺序是不能颠倒的,因为IOException是FileNotFoundException的父类。
            // 如果颠倒后FileNotFoundException的catch就不会被执行了。而且编译器会直接报错。
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到!");
        } catch (IOException e){
            System.out.println("文件无法读取!");
        }
    }
}

JDK8新特性:

try {
    new FileInputStream("C:\\Users\\friendship\\Desktop\\jjjj");
    System.out.println(100 / 0);  // 运行时异常,编写程序代码的时候可以管,也可以不管。
    // 这里的catch语句为JDK8的新特性,JDK7是不支持的。
} catch (FileNotFoundException | ArithmeticException e) {
    System.out.println("文件未找到或者算术运行异常!");
}
18.7 异常对象常用方法
package com.zh0u.exception;

/**
 * 异常对象有两个非常重要的方法:
 *      获取异常简单的描述信息:
 *          String msg = exception.getMessage();
 *      打印异常追踪的堆栈信息:
 *          exception.printStackTrack();
 */
public class ExceptionTest08 {
    public static void main(String[] args) {
        // 这里只是为了测试getMessage()方法和printStackTrace()方法。
        // 这里只是new了异常对象,但是没有将异常对象抛出。JVM会认为这是一个普通的Java对象。
        NullPointerException e = new NullPointerException("空指针异常!");

        // 获取异常简单的描述信息:这个信息实际上就是构造方法上面的String参数
        String msg = e.getMessage();
        System.out.println(msg); // 空指针异常!

        // 打印异常堆栈信息
        // Java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印(多线程时会介绍)。
        e.printStackTrace(); // java.lang.NullPointerException: 空指针异常!
        //                          at com.zh0u.exception.ExceptionTest08.main(ExceptionTest08.java:7)

    }
}

当异常发生是,查看有exception.printStackTrack()打印出的异常信息,应该是从上往下看。

18.8 finally语句

放在finally语句块中的代码是一定会执行的。除非遇到:System.exit(0)退出Java虚拟机。

package com.zh0u.exception;

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

/*
* finally语句需要与try...catch(catch可省略)语句一起使用,在这三个语句块中,finally是放在最后,而且finally语句中代码块是一定会被还行的。
* */
public class ExceptionTest10 {
    public static void main(String[] args) {
        FileInputStream fileInputStream = null; // 创建文件输入流对象
        try {
            fileInputStream = new FileInputStream("C:\\Users\\friendship\\Desktop\\不存在的文件.jpg");
            // 创建一个 null 字符串
            String s = null;
            s.toString(); // 由于 s 是null,这里在执行s.toString()代码的时候会出现空指针异常。

            // 关闭文件流
            // 在这里关闭文件流是危险的,因为只要上面的代码出现异常,文件流就不能关闭了。应该放在finally语句中去关闭。
            //fileInputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 在finally语句中的代码块是一定会执行的。文件流的关闭通常就是放在这个语句块里面进行关闭。
            try {
                if (fileInputStream != null)
                        fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("我在finally语句中,我一定会被执行!!!"); // 我在finally语句中,我一定会被执行!!!
        }
    }
}

try代码块中有return语句,finally代码块也会执行。

package com.zh0u.exception;

/*try代码块中有return语句,finally代码块也会执行。*/
public class ExceptionTest11 {
    public static void main(String[] args) {
        /*
         try语句和finally,没有catch语句也是可以的。
         try不能单独使用,try...finally可以联合使用。
         */
        // 以下代码的执行顺序:
        //      先执行try...
        //      在执行finally...
        //      最后执行 return (return语句只要执行方法必然结束)。
        try{
            System.out.println("try...");
            return;
        } finally {
            // finally中的语句会执行,能执行到。
            System.out.println("finally...");
        }
    }
}

finally的重要面试题目

package com.zh0u.exception;

public class ExceptionTest12 {
    public static void main(String[] args) {
        int result = m();
        System.out.println(result); // 100
    }
    /*
    java语法规则(有一些规则是不能破坏的,一旦这么说了就必须这么做!)
        java中有一条这样的规则:
            方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法)
        java中还有一条语法规则:
            return语句一旦执行,整个方法必须结束(亘古不变的语法)
     */
    public static int m(){
        int i = 100;
        try {
            // 这行代码出现在int i = 100;的下面,所有最终的结果必须返回的是100.
            // return语句还不行保证是最后执行的。一旦执行,整个方法结束。
            return i;
        } finally {
            i++;
        }
    }
}
/*
// 反编译class字节码文件之后得到的代码如下:
public static int m(){
    int i = 100;
    int j = i;
    i++;
    return j;
}

 */
18.9 final finally finalize的区别
package com.zh0u.exception;

/**
final finally finalize有什么区别?
    final 关键字:
        final修饰的类无法被继承
        final修饰的方法无法覆盖
        final修饰的变量不能重新复制

    finally 关键字:
        和try一起联合使用。
        finally语句块中的代码块是一定会执行的。

    finalize 标识符(方法名)
        是Object类中的方法名
        这个方法是有垃圾回收器GC负责调用。
 */
public class ExceptionTest13 {
    public static void main(String[] args) {
        // final是一个关键字,表示最终的、不变的。
        final int i = 200; // 复制后就无法再重新赋值。也可以在构造方法中进行赋值。

        // finally也是一个关键字,和try联合使用,使用在异常处理机制中.
        // 在finally语句块中的代码是一定会执行的。
        try{
            System.out.println("Java is best the language!");
        } finally {
            System.out.println("finally....");
        }

        // finalize()是Object类中的一个方法。作为方法名出现。
        // 所以finalize是标识符
        // finalize()是由GC垃圾回收器调用。
    }
}
18.10 自定义异常

MyException.java

package com.zh0u.exception;

/*
1、SUN提供了JDK内置的异常肯定是不够用的。在实际的开发中,有很多业务,这些业务出现异常之后,JDK中都是没有的。
那么我们可以自定义异常类吗?答案是可以的。

2、Java中怎么定义异常?
    两步:
        第一步:编写一个类继承Exception(编译时异常)或者RunTimeException(运行时异常)。
        第二步:提供两个构造方法,一个无参构造方法,一个带有String的参数的有参构造方法。
 */
public class MyException extends Exception{ // 编译时异常
    public MyException(){

    }
    public MyException(String s){
        super(s);
    }
}

ExceptionTest15.java

package com.zh0u.exception;

public class ExceptionTest15 {
    public static void main(String[] args) {
        // 创建异常对象(只new了异常对象,并没有手动抛出)
        MyException e = new MyException("用户名不能为空!");

        // 打印异常堆栈信息
        e.printStackTrace();

        // 打印异常简单描述信息
        System.out.println(e.getMessage());
    }
}
18.11 自定义异常在实际中的应用

MyStackOperationException.java

package com.zh0u.exception;

public class MyStackOperationException extends Exception{ // 直接继承Exception,即编译时异常。

    // 构造方法中必须调用super()方法
    public MyStackOperationException(){
        super();
    }

    public MyStackOperationException(String s){
        super(s);
    }
}

MyStack.java

package com.zh0u.exception;

public class MyStack {
    // 使用一维数组模拟栈存储元素的方法
    // 使用Object数组,表示什么都可以往里面放
    private Object[] elements;
    // 栈帧
    private int index;

    public MyStack() {
        elements = new Object[10];  // 初始化指定一维数组的长度为 10
        index = -1; // 初始化栈帧,栈帧为 -1 表示刚开始的时候不存在
    }

    public Object[] getObjects() {
        return elements;
    }

    public void setObjects(Object[] objects) {
        this.elements = objects;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    /**
     * 模拟压栈动作。当栈帧大于等于数组长度-1时,表示压栈失败。
     * @param obj 需要压入栈的元素。
     */
    public void push(Object obj) throws MyStackOperationException{
        if (this.index >= elements.length - 1){
            // 此处使用自定义的异常进行异常的抛出(可以将栈满导致无法“压栈”看做是一个异常)
            /*改造前:
            System.out.println("压栈失败!栈已满!");
            return;*/

            // 改造后:
            // 如果栈已满无法压栈,这里抛出一个异常。通过由于是“编译时异常”我们在push()方法中也进行抛出。
            // 创建异常对象:new MyStackOperationException("压栈失败,栈已满!");
            // 通过throw手动将异常抛出去:throw new MyStackOperationException("压栈失败,栈已满!");
            throw new MyStackOperationException("压栈失败,栈已满!");
        }
        // 程序能够执行到此处说明栈没有满

        elements[++index] = obj;  // 将元素压入栈
        System.out.println("压栈" + elements[index] + "元素成功!" + "栈帧指向" + index);
    }

    /**
     * 模拟弹栈动作,当栈帧小于0时,表示弹栈失败。
     * @return 弹栈是否成功
     */
    public void pop() throws MyStackOperationException {
        if (this.index < 0){
            /*System.out.println("弹栈失败,栈已空!");
            return false;*/
            throw new MyStackOperationException("弹栈失败,栈已空!");
        }
        System.out.println("弹栈" + elements[index] + "元素成功!" + "栈帧指向" + index);
        elements[index--] = null;  // 弹出栈元素

    }
}

ExceptionTest16.java

package com.zh0u.exception;

public class ExceptionTest16 {
    public static void main(String[] args) {
        MyStack myStack = new MyStack();
        // 压栈
        try {
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            myStack.push(new Object());
            // 程序到此处异常
            myStack.push(new Object());
        } catch (MyStackOperationException e) {
            e.printStackTrace();
            System.out.println(e.getMessage());
        }

        // 弹栈
        try {
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            myStack.pop();
            // 程序到此处异常
            myStack.pop();
        } catch (MyStackOperationException e) {
//            e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }
}

方法覆盖问题:

重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少,甚至是不抛都是可以的。

父类不抛异常,子类重写可以抛RunTimeException。

class Animal{
    public void doSome(){
        
    }
}
class Cat extends Animal{
    public void doSome() throws RuntimeException{  // 编译正常
        
    }
}
18.12 异常关键字总结
异常捕捉:
    try
    catch
    finally
方法声明上使用:
	throws
手动抛异常使用:
	throw
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值