个人笔记记录

面向对象

Java的核心思想就是OOP (Object Oriented Programming) 即面向对象编程

什么是面向对象

面向过程 & 面向对象

面向过程思想

  • 步骤清晰简单,第一步做什么,第二步做什么…

  • 面对过程适合处理一些较为简单的问题

面向对象思想

  • 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。最后,才对某个分类下的细节进行面向过程的思索。

  • 面向对象适合处理复杂的问题,适合处理需要多人协作的问题。

对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,我们需要使用面向对象的思路来分析整个系统。但是,具体到微观操作,仍然需要面向过程的思路去处理。

什么是面向对象

面向对象编程(Object-Oriented Programming, OOP)

面向对象编程的本质就是:以类的方式组织代码,以对象的形式封装数据

实现方法:抽象(具体到一般化的过程)

三大特性:

  • 封装
  • 继承
  • 多态

从认识论角度考虑是先有对象后有类。对象,是具体的事物。类,是抽象的,是对对象的抽象

从代码运行角度考虑是先有类后有对象。类是对象的模板。

回顾方法的定义

方法的定义

  • 修饰符
  • 返回类型
  • break:跳出switch,结束循环和return的区别
  • 方法名:注意规范就OK 见名知意
  • 参数列表:(参数类型,参数名)
  • 异常抛出(异常抛出后面再讲)
package com.kimtanyo.oop.demo01;

import java.io.IOException;

//Demo01
public class Demo01 {
    //main方法
    //一个程序其实一般只有一个java文件中有main方法,其他java文件只是存放不同类用来调用
    public static void main(String[] args) {
        Demo01 temp = new Demo01();
        System.out.println(temp.sayHello());

    }
    /*
    修饰符 返回值类型 方法名(...){
        //方法体
        return 返回值;
    }
     */
    public String sayHello(){
        return "hello, world";
    }
    public void printHello(){
        System.out.println("hello, world");
    }

    public int max(int a, int b){
        return a>b ? a : b;
    }

    
    // 数组下标越界:ArrayIndexOutOfBounds

    public void readFile(String file) throws IOException{
        
    }


}

回顾方法的调用

方法的调用

  • 静态方法
  • 非静态方法
  • 形参和实参
  • 值传递和引用传递
  • this关键字(留到继承说)
package com.kimtanyo.oop.demo01;

public class Demo02 {

    public static void main(String[] args) {
        //静态方法调用
        Student.staticSay();

        //非静态方法调用
        //实例化这个类 new
        //对象类型 对象名 = 对象值
        Student kimtanyo = new Student();
        kimtanyo.say();

    }

    //静态方法是和类一起加载的
    public static void a(){
        Demo02 temp = new Demo02();
        temp.b();
        //不能直接调用 b();
    }
    //非静态方法是类实例化之后才存在
    public void b(){
        a();
    }
}

package com.kimtanyo.oop.demo01;

public class Demo03 {

    public static void main(String[] args) {
        //类自己的非静态方法也需要实例化自己之后才能调用
        int result = new Demo03().add(1, 2);
        System.out.println(result);
    }

    public int add(int a, int b){
        return a+b;
    }
}

package com.kimtanyo.oop.demo01;

//值传递
public class Demo04 {
    public static void main(String[] args) {
        int a = 1;
        System.out.println(a);
        change(a);
        System.out.println(a);
    }

    // 返回值为空
    public static void change(int a){
        a = 10;
    }
}

/*
1
1
 */
package com.kimtanyo.oop.demo01;

//引用传递:对象,本质还是值传递(只是值是对象的地址)
public class Demo05 {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.name);
        change(person);
        System.out.println(person.name);
    }

    public static void change(Person person){
        //实参传递给形参的是实例化对象的地址 因此改变该对象的属性会影响实参
        person.name = "kimtanyo";
        //但是注意以下这种是不会改变的 因为new把形参person指向了新的地址
        // person = new Person();
        // person.name="kimtanyo";
    }
}

//一个java文件只能有一个public class但是可以有多个class
class Person{
    //Person类,定义了一个属性:name
    String name;
}

/*
null
kimtanyo
 */

类与对象的创建

类与对象的关系

类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物.

  • 动物、植物、手机、电脑……

  • Person类、Pet类、Car类等,这些类都是用来描述/定义某一类具体的事物应该具备的特点和行为

对象是抽象概念的具体实例

  • 张三就是人的一个具体实例,张三家里的旺财就是狗的一个具体实例。

  • 能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念.

创建与初始化对象

使用new关键字创建对象

使用new关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中构造器的调用。

同java类的实例变量在非静态方法中不用实例化也可以直接调用,但是跨java类直接调用不行

package com.kimtanyo.oop.demo02;
//学生类
public class Student {

    //属性:字段
    String name;// null
    int age;// 0

    //方法
    public void study(){
        System.out.println(this.name+"在学习");
        // 实例变量在静态方法中必须实例化才能调用 但是在非静态方法中不用
        // 这里不加this也可以
    }

    // this关键字指向的是当前对象的引用
    // this.属性名称 指的是访问类中的成员变量,用来区分成员变量和局部变量(重名问题)
    // 有点像python的self.属性名称
}

package com.kimtanyo.oop.demo02;

//一个项目应该只存在一个main方法
public class Application {
    public static void main(String[] args) {

        //类:抽象的 --实例化--> 对象:具体的
        //类实例化之后会返回一个自己的对象
        //xz和wyf就是两个Student类的具体实例

        Student xz = new Student();
        Student wyf = new Student();

        xz.name = "XiaoZhan";
        xz.age = 3;

        System.out.println(xz.name);
        System.out.println(xz.age);

        wyf.name = "WuYiFan";
        wyf.age = 100;

        System.out.println(wyf.name);
        System.out.println(wyf.age);




    }
}

构造器详解

使用new关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中构造器的调用。

类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有以下俩个特点:

  • 必须和类的名字相同(唯一首字母大写的方法

  • 必须没有返回类型,也不能写void

构造器必须要掌握

反编译的另一种方式:Project Structure --> Project Settings --> Modules --> Add Content Root 在其中添加项目的out输出路径,即可在IDEA的左侧Project Structure中找到out路径内的所有class文件

查看Person.class可以发现,就算Person类里什么都不写,Application.java编译过后,Person.class中依然有一个默认的public Person()方法,就是Person类的默认构造器

image-20210726040853527

构造器的调用是通过new 关键字来调用的,例如 Person person = new Person();, 构造函数作用是为对象分配空间,并初始化对象的属性

当定义一个类的时候,如果这个类中没有定义构造函数,则系统会为该类提供一个默认的无参数的构造函数

image-20210728183129724

构造函数的核心作用

  1. 使用new关键字的本质是在调用构造器方法(设置断点debug步进可以验证),给对象分配空间
  2. 初始化属性

有点像python的def __init__(self):

package com.kimtanyo.oop.demo02;

public class Person {

    String name;

}
----------------------------------
package com.kimtanyo.oop.demo02;

//一个项目应该只存在一个main方法
public class Application {
    public static void main(String[] args) {
        Person person = new Person();// 这里的Person()其实是调用Person类的构造器方法(默认是无参空方法)

        System.out.println(person.name);

    }
}

/*
null
*/
package com.kimtanyo.oop.demo02;

public class Person {

    String name;

    public Person(){
        this.name = "kimtanyo";
    }
}
---------------------------------

//一个项目应该只存在一个main方法
public class Application {
    public static void main(String[] args) {
        Person person = new Person();// 这里的Person()其实是调用Person类的自定义构造器方法

        System.out.println(person.name);

    }
}

/*
kimtanyo
*/    

构造函数也是可以重载的

如果自己在类中定义了一个带参数的构造函数,则系统的默认无参数构造函数就不会提供,那么创建对象的时候,就不能使用无参数的构造函数了(除非显式地定义无参数的构造器)

package com.kimtanyo.oop.demo02;
public class Person {

    //一个类即使什么都不写也会存在一个默认的无参构造器方法
    //可以显式地定义构造器

    String name;

    //实例化初始值
    //1. 使用new关键字,本质是在调用构造器
    public Person(){
        this.name = "kimtanyo";
    }

    //有参构造:一旦定义了有参构造,无参构造就必须显式定义
    public Person(String name){
        this.name = name;
    }
}
---------------------------------

//一个项目应该只存在一个main方法
public class Application {
    public static void main(String[] args) {
        Person person = new Person("kimtanyo");// 这里的Person()其实是调用Person类的自定义构造器方法

        System.out.println(person.name);

    }
}
/*
kimtanyo
*/

一般情况下,如果定义了有参构造器,那么需要定义一个空的无参构造器防止报错

IDEA快捷键

快捷键Alt+Insert,选择constructor,可以生产构造器,

image-20210728211425310

这里选择Select None,会生成无参的默认构造器,也可以选中上面的属性然后点击OK,就会生成从方法参数赋值给对应属性的构造器

image-20210728211441850

例如选择name和age然后点击OK,就会生成

image-20210728211340707

总结

构造器:

  • 和类名相同(首字母大写)
  • 没有返回值

作用:

  • new本质在调用构造方法给对象分配空间

  • 初始化对象的值

注意点:

  • 定义有参构造之后,如果想使用无参构造,显示的定义一个无参的构造ALt+Insert

创建对象内存分析

package com.kimtanyo.oop.demo03;

public class Application {
    public static void main(String[] args) {

        Pet dog = new Pet();

        dog.name = "旺财";
        dog.age = 3;
        dog.shout();

        System.out.println(dog.name);
        System.out.println(dog.age);

        Pet cat = new Pet();
    }
}

--------------------------------
package com.kimtanyo.oop.demo03;

public class Pet {
    String name;
    int age;

    public void shout(){
        System.out.println("叫了一声");
    }
}

我们对编译执行Application.java的过程进行内存分析

  1. 在方法区加载Application类的方法、属性、常量池
  2. 在栈中加载main()方法(压在栈底,从上往下弹出栈,当main()弹出栈的时候程序基本结束)
  3. main()方法的第一步是Pet dog = new Pet();,因此加载Pet类的方法、属性、常量池到方法区
  4. 在栈里面生成一个引用变量名dog
  5. 在堆里面生成引用变量dog指向的具体对象的属性和方法,分配内存地址,初始化对象的各个属性,堆中的shout()是调用方法区内Pet类的shout()方法
  6. Pet dog = new Pet();结束
  7. 在堆中进行自定义赋值dog.name="旺财";dog.age=3,在堆中直接调用方法区内的shout()方法(因为没有参数)
  8. 然后同样Pet cat = new Pet();,在栈里面生成一个引用变量名cat
  9. 在堆里面生成引用变量cat指向的具体对象的属性和方法,分配内存地址,初始化对象的各个属性
image-20210728220913259

注意:

  • 方法区是堆的一个特殊部分

  • 栈存放方法和引用变量名

  • 堆存放具体对象

  • 静态方法区内的static方法可以被任何堆中的对象直接调用

类与对象 —— 简单补充小结

属性:也叫字段 Field 或者成员变量

char:默认初始化是空字符'',其实是\u0000

引用变量:默认初始化都是null,包括String

对象:创建对象必须使用new关键字,同时必须要有构造器

调用:属性调用形式是person.name,方法调用形式是person.score()

特殊:类的实例变量在静态方法中必须实例化才能调用,但是在非静态方法中可以直接调用

封装详解

该露的露,该藏的藏

  • 我们程序设计要追求“高内聚,低耦合”
  • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉
  • 低耦合:仅暴露少量的方法给外部使用

封装(数据的隐藏)

  • 通常,应禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问,这称为信息隐藏

记住这句话就够了:属性私有,get/set

private属性需要通过public方法get/set来调用和设置

封装的好处:

  1. 提高程序的安全性,保护数据
  2. 隐藏代码的实现细节
  3. 统一接口
  4. 系统可维护性增加
package com.kimtanyo.oop;
import com.kimtanyo.oop.demo04.Student;

/*
    1. 提高程序的安全性,保护数据
    2. 隐藏代码的实现细节
    3. 统一接口
    4. 系统可维护性增加
 */
public class Application {
    public static void main(String[] args) {
        Student s1 = new Student();

        // s1.name = ""; 会报错 无法正常调用私有属性

        s1.setName("kimtanyo");

        System.out.println(s1.getName());

        s1.setAge(999);//年龄999是不合法的
        System.out.println(s1.getAge());
    }
}
---------------------------------------------------
package com.kimtanyo.oop.demo04;

//封装大部分时候是对于属性,对方法比较少用
public class Student {

    //属性私有
    private String name;
    private int id;
    private char sex;
    private int age;

    //提供一些可以操作这个属性的方法
    //提供一些public的get、set方法

    //get 获得这个数据
    public String getName(){
        return this.name;
    }

    //set给这个数据设置值
    public void setName(String name){
        this.name = name;
    }

    //alt + insert

    public int getId() {
        return id;
    }

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

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }
    //封装的好处之一:可以避免不合法的属性
    public void setAge(int age) {
        if (age>120 || age<0){
            this.age = 3;
        }else {
            this.age = age;//说明输入的人智商就是个3岁小朋友
        }

    }
}
    

get/set可以重载,但是一般不会这么做,而是通过构造器重载

get/set快捷键

快捷键Alt+Insert

image-20210731205242281

Extra: Java中访问修饰符public、private、protect、default的范围

同一个类 同一个包 不同包的子类 不同包的非子类
Private
Default
Protected
Public
  • public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。

  • private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。

  • protect: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。

  • default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问,同一个包的子类也可以。

get/set的原理就是因为private属性只能在同一个类中进行访问,所以在同一个类中定义get/set方法进行直接调用/设置,其他地方都只能通过实例化public对象之后的get/set来间接操作

同一个java文件中只能有一个public class但是可以有多个class,也就是default class,default class不可以被不同包调用

什么是继承

  • 继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模
  • extends的意思是“扩展”,子类是父类的扩展
  • JAVA中类只有单继承,没有多继承(interface和implements可以实现多接口)
    • 继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合
    • 继承关系的俩个类,一个为子类(派生类),一个为父类(基类);子类继承父类,使用关键字extends来表示
    • 子类和父类之间,从意义上讲应该具有“A is a B”的关系
  • final修饰类不可以作为父类被子类继承,但可以作为子类继承父类
  • object类
  • super - this
  • 方法重写(override)

interface和extends的区别

interface是定义接口的关键字。
implement是实现接口的关键字。
extends是子类继承父类的关键字。

class A extends B implements C,D,E {} (class 子类名 extends 父类名 implenments 接口名)

package com.kimtanyo.oop.demo05;

//在java中,所有的类,都默认直接或间接继承Object类
//Person: 父类
public class Person {
    //一般属性是私有的,方法是公有的
    private int money = 10_000_000;

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public void say() {
        System.out.println("说了一句话");
    }
}
---------------------------------------
package com.kimtanyo.oop.demo05;
//Teacher is a Person: 派生类,子类
public class Teacher extends Person{
}
---------------------------------------
package com.kimtanyo.oop.demo05;
//Student is a Person: 派生类,子类
public class Student extends Person{
}
---------------------------------------
package com.kimtanyo.oop.demo05;

public class Application {
    public static void main(String[] args) {
        //Student身为子类,也有父类全部的方法和属性(private除外)
        Student student = new Student();
        student.say();
        //对父类的private属性用get/set来调用/设置
        System.out.println(student.getMoney());


    }
}

继承关系Hierarchy快捷键

快捷键Ctrl+H可以看到所有继承关系

image-20210731221816692

在java中,所有的类,都默认直接或间接继承Object类,也享有Object的所有protected和public方法

这里Student类直接继承的只有一个Person类,符合单继承,但是间接继承了Object类

Super - this详解

package com.kimtanyo.oop.demo05;
import java.lang.Object;
//在java中,所有的类,都默认直接或间接继承Object类
//Person: 父类
public class Person {

    protected String name = "kimtanyo";

    public void print(){
        System.out.println("Person");
    }

    public Person(){
        System.out.println("Person无参构造方法");
    }

    public Person(String name){
        System.out.println("Person有参构造方法");
    }
}
--------------------------------------------------
package com.kimtanyo.oop.demo05;
//Student is a Person: 派生类,子类
public class Student extends Person{

    private String name = "ccd";

    public void test(String name){
        System.out.println(name);
        System.out.println(this.name);
        System.out.println(super.name);
    }

    public void print(){
        System.out.println("Student");
    }

    public void test1(){
        print();
        this.print();
        super.print();
    }

    public Student(){
        //隐藏代码:默认调用了父类的无参构造(无论子类此构造是否有参) --> 即 super();
        super();//调用父类构造器必须在子类构造器的第一行
        System.out.println("Student无参构造方法");
    }
    public Student(String name){
        super("kimtanyo");//也可以调用父类的有参构造,但是必须显式调用
        //注意一定要在父类中写无参构造防止其他隐式默认调用父类无参构造的地方报错
        System.out.println("Student有参构造方法");
    }
    public Student(String name, int age){
        this("ccd");//调用单参数的子类构造器
        System.out.println("Student双参数构造方法");
    }
}
--------------------------------------------
package com.kimtanyo.oop.demo05;

public class Application {
    public static void main(String[] args) {

        Student student1 = new Student();
        System.out.println();
        Student student2 = new Student("Oliver");
        System.out.println();
        Student student3 = new Student("Oliver", 23);
        System.out.println();
        student1.test("Oliver");
        System.out.println();
        student1.test1();



    }
}

super注意点:

  1. super调用父类的构造方法,必须在构造方法的第一行
  2. super必须只能出现在子类的方法或者构造方法中
  3. super和this不能同时调用构造方法!

super Vs this:

  • 代表的对象不同:

    • this:本身调用这个对象
    • super:代表父类对象的应用
  • 前提

    • this:没有继承也可以使用
    • super:只能在继承条件才可以使用
  • 构造方法

    • this();本类的构造
    • super():父类的构造

每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错,所以当在父类中定义了其它形式的有参构造,一定要记得显式定义无参构造。

参考:https://blog.csdn.net/lncsdn_123/article/details/79025525

方法重写(Override)

重写的定义

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

Note:实际就是子类可以定义同名同参数同返回于父类的方法,但是可以自己进行一些细化更改来区分子类和父类,例如Person有lifeSpan()方法返回人的平均寿命,但是子类Teacher也想定义一个lifeSpan()返回老师的平均寿命,但是老师的平均寿命显著高于人的平均的寿命,所以需要重写

==重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。==例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。

在面向对象原则里,重写意味着可以重写任何现有方法。实例如下:

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // a是Animal类型也是Animal类的对象
      Animal b = new Dog(); // b是Animal类型但是是Dog类的对象
 
      a.move();// 执行 Animal 类的方法
 
      b.move();//执行 Dog 类的方法
   }
}

以上实例编译运行结果如下:

动物可以移动
狗可以跑和走

在上面的例子中可以看到,尽管 b 属于 Animal 类型,但是它运行的是 Dog 类的 move方法

这是由于在编译阶段,只是检查参数的引用类型

然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法

因此在上面的例子中,之所以能编译成功,是因为 Animal 类中存在 move 方法,然而运行时,运行的是特定对象的方法

父类数据类型 对象名 = new 子类名();实际是多态

这样定义的对象,仍旧属于父类的引用类型,所以调用的方法一定得是父类里有的,否则无法通过编译(如果不要求重写,调用的方法可以是父类有但子类没有的,因为子类继承父类的所有非private方法,这里后面多态部分会解释)

以下例子:

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
   public void bark(){
      System.out.println("狗可以吠叫");
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
 
      a.move();// 执行 Animal 类的方法
      b.move();//执行 Dog 类的方法
      b.bark();
   }
}

以上实例编译运行结果如下:

TestDog.java:30: cannot find symbol
symbol  : method bark()
location: class Animal
                b.bark();
                 ^

该程序将抛出一个编译错误,因为b的引用类型Animal没有bark方法。

(这里可以将bark()放到Animal中,如果Dog也有bark(),那么b.bark()执行Dog类的bark(),如果Dog没有bark(),那么b.bark()执行Animal类的bark())

方法的重写规则

  • 参数列表与被重写方法的参数列表必须完全相同。
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个类,则不能重写该类的方法。

Super 关键字的使用

当需要在子类中调用父类的被重写方法时,要使用 super 关键字。

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      super.move(); // 应用super类的方法
      System.out.println("狗可以跑和走");
   }
}
 
public class TestDog{
   public static void main(String args[]){
 
      Animal b = new Dog(); // Dog 对象
      b.move(); //执行 Dog类的方法
 
   }
}

以上实例编译运行结果如下:

动物可以移动
狗可以跑和走

为什么Static方法不能被重写

方法重写只和非静态方法有关,和静态方法无关

因为静态方法是和类一起加载的,非静态方法是和实例对象一起加载的

对象b调用静态方法,是调用B类的方法,因为b是用B类作为引用数据类型定义的

对象b调用非静态方法,是调用A类的方法,因为b是通过A类的构造器new出来的对象

例子如下:

package com.kimtanyo.oop.demo05;

//重写都是方法的重写,和属性无关
public class B {
    public void test(){
        System.out.println("B=>test()");
    }

    public static void test2(){
        System.out.println("B=>test()");
    }
}

--------------------------
package com.kimtanyo.oop.demo05;

public class A extends B{
    //重写快捷键:Alt+Insert --> Override Methods

    public void test() {
        System.out.println("A=>test()");
    }

    public static void test2() {
        System.out.println("A=>test()");
    }
}

--------------------------
package com.kimtanyo.oop.demo05;

public class Application3 {

    //静态的方法和非静态的方法区别很大
        //静态方法:方法的调用只和左边类名定义的数据类型有关
    public static void main(String[] args) {

        A a= new A();
        a.test();//A

        //父类的引用指向了子类的类型
        B b =new A(); //子类重写了父类的方法
        b.test();//A
		
        //静态方法不适用
        a.test2();//A
        b.test2();//B
    }
}

重写的快捷键

依然为Alt+Insert,选择Override Methods

在IDEA中观察箭头可以发现子类A的非静态方法test()重写了父类B的非静态方法test()

image-20210803214252245

什么是多态

  • 即同一方法可以根据发送对象的不同而采用多种不同的行为方式。
  • 一个对象的实际类型是确定的,但可以指向对象的引用的类型有很多(父类或有关系的类)
  • 多态存在的条件
    • 有继承关系
    • 子类重写父类方法(static、final、private等无法重写的情况不存在多态)
    • 父类引用指向子类对象:Parent p = new Child();
  • 注意:多态是方法的多态,属性没有多态性。
  • instanceof (类型转换) 引用类型

方法能否执行取决于引用类型(左侧),执行内容取决于实际类型(右侧)

Extra: .getClass()方法获得的是实际类型不是引用类型

package com.kimtanyo.oop.demo06;

public class Application {
    public static void main(String[] args) {
        //一个对象的实际类型是确定的
        //new student;
        //new person;

        //但是指向的引用类型就不确定了:父类的引用指向子类对象

        //Student 能调用的方法是自己的和继承父类的
        Student s1 = new Student();
        //Person 可以指向子类但是不能调用子类独有的方法
        Person s2 = new Student();//引用类型Person,实际类型Student
        Object s3 = new Student();//引用类型Object,实际类型Student

        //s2.run() 子类Student中没有run()时,子类直接继承父类的方法来执行
        //s2.run() 子类Student重写了run()时,子类执行了子类自己的方法
        s2.run();
        s1.run();

        //s2.eat() 当引用类型Person中没有eat()时,对象无法调用eat()
        ((Student) s2).eat();//强制把Person引用类型转换成了Student引用类型 高转低需要强制声明
        s1.eat();

        //总结:方法能否执行取决于引用类型(左侧),执行内容取决于实际类型(右侧)
    }
}
class Student extends Person{
    @Override
    public void run() {
        System.out.println("son");
    }
    public void eat(){
        System.out.println("ear");
    }
}
class Person {
    public void run(){
        System.out.println("run");
    }
}

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

instanceof和类型转换

instanceof作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型

x instanceof Y语法要求x指向的实际类型和Y有继承或间接继承关系,否则编译报错
(注意父类实际对象x和子类Y x instanceof Y 也不会报错,只是结果是False)

总结:x instanceof Y编译通不通过取决于引用类型(看左侧),True or False取决于实际类型(看右侧)

强制转换是父类转子类(Child) parent

Extra: (Child) parent强制转换转的是引用类型不是实际类型,用.getClass()方法可以验证实际类型没变;

e.g.:

//背景: A -> B -> C ->D 且 ABCD都有不同的go()方法重写

A t = new D();

((C) t).go(); //执行的是D中的go(),因为((C) t)的实际类型还是D,转换的只是引用类型从A到C,只要C中有go()编译就能通过,但是运行的仍然是D的go()

B t1 = (C) t; //相当于强制转换t的实际类型从A到C,然后再自动转换实际类型从C到B,赋值给t1
package com.kimtanyo.oop.demo06;

class Person {
    public void run(){
        System.out.println("run");
    }
}

class Teacher extends Person {
}

class Student extends Person{
    public void go(){
        System.out.println("go");
    }
}

package com.kimtanyo.oop.demo06;

public class Application {
    public static void main(String[] args) {

        //instanceof 类型转换
        System.out.println("====================");

        //Object > String
        //Object > Person > Teacher
        //Object > Person > Student
        Object object = new Student();

        System.out.println(object instanceof Student);//True
        System.out.println(object instanceof Person);//True
        System.out.println(object instanceof Object);//True
        System.out.println(object instanceof Teacher);//False
        System.out.println(object instanceof String);//False

        System.out.println("====================");
        Person person = new Student();

        System.out.println(person instanceof Student);//True
        System.out.println(person instanceof Person);//True
        System.out.println(person instanceof Object);//True
        System.out.println(person instanceof Teacher);//False
        // System.out.println(person instanceof String);//编译报错

        /*  x instanceof Y 语法要求x指向的实际类型和Y有继承或间接继承关系,否则编译报错
            注意父类实际对象x和子类Y x instanceof Y 也不会报错
        * */

        /*这里person instanceof Teacher编译不报错标红的根本原因是
          在编译阶段,只是检查参数的引用类型,因为person的引用类型是Person,跟Teacher有继承关系所以编译通过
          然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法(实际类型)
        */

        System.out.println("====================");
        Student student = new Student();

        System.out.println(student instanceof Student);//True
        System.out.println(student instanceof Person);//True
        System.out.println(student instanceof Object);//True
        // System.out.println(student instanceof Teacher);//编译报错
        // System.out.println(person instanceof String);//编译报错

        //总结:instanceof 编译通不通过取决于引用类型(看左侧),True or False取决于实际类型(看右侧)

        //类型之间的转化:父 子
        System.out.println("====================");

        //高              低
        Person obj = new Student();

        //将obj转换为Student引用类型,就可以使用Student类型的方法了
        Student objStu = (Student) obj;
        objStu.go();
        //也可以省略命名赋值,合成一句话
        ((Student) obj).go();

        //子类转换成父类,可能会丢失自己本来的一些方法
        Student stu = new Student();
        stu.go();
        Person per = stu;
        // per.go(); //编译标红报错



    }
}

这里person instanceof Teacher编译不报错标红的根本原因是:
在编译阶段,只是检查参数的引用类型,因为person的引用类型是Person,跟Teacher有继承关系所以编译通过
然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法(实际类型)

static关键字详解

后续在注解和反射中会讲到类的加载顺序原理

package com.kimtanyo.oop.demo07;

public class Student {

    private static int age;
    private double score;

    public void run(){

    }
    public static void go(){

    }

    public static void main(String[] args) {
        Student s1 = new Student();

        System.out.println(Student.age);
        System.out.println(s1.score);
        // System.out.println(Student.score);//编译错误
        // System.out.println(s1.age);//可以但不推荐


        // run;//编译错误
        new Student().run();
        Student.go();
        go();//直接调用方法 = 寻找当前类的静态方法

        //后续在注解和反射中会讲到类的加载顺序原理
    }
}

匿名代码块和静态代码块

代码块(匿名代码块)

方便加载初始化数据
程序执行的时候并不能主动调用代码块
创建对象的时候自动调用,在构造器之前

静态代码块
类加载的时候调用且仅调用一次

执行顺序:静态代码块、匿名代码块、构造方法

package com.kimtanyo.oop.demo07;

public class Person {
    
    {
        System.out.println("匿名代码块");
    }
    static{
        System.out.println("静态代码块");
    }
    public Person(){
        System.out.println("构造方法");
    }

    public static void main(String[] args) {
        Person person = new Person();
        System.out.println("======================");
        Person person2 = new Person();
    }
}

image-20210815062319716

可以看出来执行顺序是静态代码块、匿名代码块、构造方法,且只静态代码块只加载一次

静态导入包

package com.kimtanyo.oop.demo07;

//静态导入包:只可以导入静态方法和静态类属性,通常用于方便简略书写
import static java.lang.Math.random;
import static java.lang.Math.PI;

public class Test {
    public static void main(String[] args) {
        System.out.println(random());//如果没有静态导入需要Math.random()
        System.out.println(PI);//如果没有静态导入需要Math.PI
    }
}

抽象类(abstract class)

  • abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法;如果修饰类,那么该类就是抽象类。
  • 抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类。
  • 抽象类,不能使用new关键字来创建对象,它是用来让子类继承的。
  • 抽象方法,只有方法的声明,没有方法的实现,它是用来让子类实现的。
  • 子类继承抽象类,那么就必须要实现抽象类没有实现的抽象方法,否则该子类也要声明为抽象类。

定义抽象方法没有大括号的方法实现,在参数列表后直接加分号结束定义

注意:抽象类是有构造器的,区别于接口没有构造器

(Python中的抽象类在abc.ABCMeta,抽象方法在@abc.abstractmethod)

抽象类的作用

约束类,使类必须含有某些方法且可以根据子类不同而自定义这些方法的具体实现(普通的继承重写无法约束子类必须含有这些方法),因此在父类中声明抽象方法名、返回值、参数,在子类中重写并具体实现

package com.kimtanyo.oop.demo08;

// abstract 抽象类 受限制于单继承 接口可以多实现
abstract class Action {
    //约束 在其他类中实现
    //abstract 抽象方法 只有方法声明 没有方法实现
    public abstract void doSomething();

}

// 抽象类的所有抽象方法,继承的子类必须全部实现,除非子类也是抽象类
class A extends Action{
    @Override
    public void doSomething() {

    }
}

public class Application {
    public static void main(String[] args) {
        new A();
        // new Action(); //报错 抽象类无法实例化
    }
}

接口的定义与实现

普通类:只有具体实现

抽象类:具体实现和规范(抽象方法)都有

接口:只有规范,自己无法写方法(约束和实现分离:企业中的面向接口编程)

接口就是规范,定义的是一组规则,体现了现实世界中“如果你是..则必须能…”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你好人,则必须干掉坏人;如果你是坏人,则必须欺负好人。

接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。

Object Oriented 的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如c++、java、c#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。

  • 声明类的关键字是class,声明接口的关键字是interface
  • 接口中的所有方法定义其实都是抽象的,默认添加了隐式修饰符 public abstract
  • 接口中的所有属性定义其实都是常量,默认添加了隐式修饰符 public static final
  • 接口不能被实例化,没有构造方法
  • implements 可以实现多接口
  • 接口中的所有方法都必须要重写
package com.kimtanyo.oop.demo09;

// interface 定义的关键字 接口都需要有实现类
public interface UserService {
    // 接口中的所有方法定义其实都是抽象的 默认添加了隐式修饰符 public abstract
    void add(String name);
    void delete(String name);
    void update(String name);
    void query(String name);

    //接口中的所有属性定义其实都是常量 默认添加了隐式修饰符 public static final
    int AGE = 99;

}

------------------------------------------
    
package com.kimtanyo.oop.demo09;

public interface TimeService {
    void timer();
}

------------------------------------------
    
package com.kimtanyo.oop.demo09;

// 类可以实现接口 implements 接口
// 实现了接口的类,就需要重写接口中的方法
// 抽象类 extends 只能单继承
// 接口 implements 可以多实现
public class UserServiceImplement implements UserService, TimeService {
    @Override
    public void add(String name) {

    }

    @Override
    public void delete(String name) {

    }

    @Override
    public void update(String name) {

    }

    @Override
    public void query(String name) {

    }

    @Override
    public void timer() {

    }
}

N种内部类

内部类就是在一个类的内部在定义一个类,比如,A类中定义一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类了。

  1. 成员内部类

  2. 静态内部类

  3. 局部内部类

  4. 匿名内部类

后续讲lambda表达式会再详细讲到

package com.kimtanyo.oop.demo10;

//外部类
public class Outer {

    private int id = 10;
    private void out(){
        System.out.println("这是外部类的方法");
    }

    //成员内部类
    public class Inner{
        public void in(){
            System.out.println("这是内部类的方法");
        }

        // 获得外部类的私有属性
        public void getId(){
            System.out.println(Outer.this.id); // Outer.this.id等价于id
        }
        // 获得外部类的私有方法
        public void getOut(){
            Outer.this.out(); // Outer.this.out()等价于out()
        }

    }
    //静态内部类
    public static class Inner1 {
        public void getId() {
            // System.out.println(Outer.this.id);
            // 报错 因为Inner1是静态类 优先于外部类Outer被加载 此时还没有Outer以及其属性id

        }
    }
    //局部内部类(外部类的方法中定义内部类)
    public void method(){
        class Inner2{
            public void in(){
                System.out.println("这是局部内部类的方法");
            }
        }
    }
    //匿名内部类在Test.java中讲解
}

//一个java类中可以有多个class类,但是只能有一个public class
//普通class类中可以写main方法,但是不推荐
class A{
    public static void main(String[] args) {
        Outer outer = new Outer();

        // 通过外部类来实例化内部类
        Outer.Inner inner = outer.new Inner();
        inner.getId();
        inner.getOut();

    }
}
package com.kimtanyo.oop.demo10;

public class Application {
    public static void main(String[] args) {
        // new
        Outer outer = new Outer();

        // 通过外部类来实例化内部类
        Outer.Inner inner = outer.new Inner();
        inner.getId();
        inner.getOut();

    }
}

package com.kimtanyo.oop.demo10;

//匿名内部类
public class Test {
    public static void main(String[] args) {
        // 没有名字的初始化类实例 不用将实例保存到变量中
        new Apple().eat();

        // 实现了接口的内部类实例 只是没有类名也没有实例名
        new UserService(){
            @Override
            public void hello() {

            }
        };

        // 返回的对象 实现了UserService接口的userService 但是没有实现接口的类名 直接返回了对象
        UserService userService = new UserService(){
            @Override
            public void hello() {

            }
        };
        // 在花括号内定义实现接口的类 在外部可以通过返回的对象直接调用
        userService.hello();
    }
}

class Apple{
    public void eat(){
        System.out.println("eat");
    }
}

interface UserService{
    void hello();
}

异常

Error和Exception

什么是异常

实际工作中,遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求、你的程序要打开某个文件,这个文件可能不存在或者文件格式不对,你要读取数据库的数据,数据可能是空的等。我们的程序再跑着,内存或硬盘可能满了。等等。

软件程序在运行过程中,非常可能遇到刚刚提到的这些异常问题,我们叫异常,英文是:
Exception,意思是例外。这些,例外情况,或者叫异常,怎么让我们写的程序做出合理的处理。而不至于程序崩溃。

异常指程序运行中出现的不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。

异常发生在程序运行期间,它影响了正常的程序执行流程。

简单分类

要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:

  • 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常:运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误Error:错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

异常体系结构

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。

在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception

所有的异常类是从 java.lang.Exception 类继承的子类。

Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error 。

Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。

异常类有两个主要的子类:IOException 类和 RuntimeException 类。

image-20210817071549725

Error

Error类对象由Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。

Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;

还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。

Exception

在Exception分支中有一个重要的子类RuntimeException(运行时异常)

  • ArraylndexOutOfBoundsException(数组下标越界)
  • NullPointerException(空指针异常)
  • ArithmeticException(算术异常)
  • MissingResourceException(丢失资源)
  • ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。

这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;

Error和Exception的区别:Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。

捕获和抛出异常

  • 抛出异常

  • 捕获异常

  • 异常处理5个关键字:

    • try
    • catch
    • finally
    • throw
    • throws

捕获异常

使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。

try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:

try
{
   // 程序代码
}catch(ExceptionName e1)
{
   //Catch 块
}

Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。

如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。

多重捕获块

一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。

多重捕获块的语法如下所示:

try{
   // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}catch(异常类型3 异常的变量名3){
  // 程序代码
}

上面的代码段包含了 3 个 catch块。

可以在 try 语句后面添加任意数量的 catch 块。

如果保护代码中发生异常,异常被抛给第一个 catch 块。

如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。

如果不匹配,它会被传递给第二个 catch 块。

如此,直到异常被捕获或者通过所有的 catch 块。

throws/throw 关键字:

如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。

throws 作用是提醒此方法可能会抛出这些异常,调用此方法的时候需要处理这些异常

也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。

下面方法的声明抛出一个 RemoteException 异常:

import java.io.*;
public class className
{
  public void deposit(double amount) throws RemoteException
  {
    // Method implementation
    throw new RemoteException();
  }
  //Remainder of class definition
}

一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。

例如,下面的方法声明抛出 RemoteException 和 InsufficientFundsException:

import java.io.*;
public class className
{
   public void withdraw(double amount) throws RemoteException,
                              InsufficientFundsException
   {
       // Method implementation
   }
   //Remainder of class definition
}

finally关键字

finally 关键字用来创建在 try 代码块后面执行的代码块。

无论是否发生异常,finally 代码块中的代码总会被执行。

在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。

finally 代码块出现在 catch 代码块最后,语法如下:

try{
  // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}finally{
  // 程序代码
}

注意下面事项:

  • catch 不能独立于 try 存在。
  • 在 try/catch 后面添加 finally 块并非强制性要求的。
  • try 代码后不能既没 catch 块也没 finally 块。
  • try, catch, finally 块之间不能添加任何代码。

例子

package com.kimtanyo.exception;

public class Test {
    public static void main(String[] args) {
        int a = 1;
        int b = 0;

        try{ // try 监控区域
            System.out.println(a/b);// ArithmeticException
        }catch (ArithmeticException e){ // catch 捕获异常
            System.out.println("算数异常");
        }finally { // 处理善后工作
            System.out.println("finally");
        }

        // 多重捕获块 try后面有多个catch 但是最多只能捕获其中一个
        try {
            new Test().c();// StackOverflowError
        }catch (Error e) {
            System.out.println("Error");
        }catch (Exception e) { // catch(想要捕获的异常类型 异常类的实例变量名)
            System.out.println("Exception");
        }catch (Throwable e){ // 最大的异常放在最后 否则编译报错 因为子类继承了父类 捕获了前面的异常就不会捕获后面的了 类似if … else if … else …
            System.out.println("Throwable");
        } finally {
            System.out.println("finally");
        }
        // 所有的异常都是Exception的子类 所有的错误都是Error的子类 Exception和Error都是Throwable的子类

        try {
            new Test().test(1,0);
        } catch (ArithmeticException e) {
            e.printStackTrace();
            System.out.println(e);
        }

        try {
            new Test().test(1,0);
        } catch (ArithmeticException e) {
            throw e;
            // e.printStackTrace(); 主动抛出异常所在代码块的同层次后面不能再有后续代码 会报错
        }
        System.out.println("主动抛出异常后,后面代码不会再运行");// 主动抛出异常后程序终止 非同层次的后续代码也不会再运行
    }
    public void c(){d();}
    public void d(){c();}

    // 在方法上抛出异常 关键字 throws 作用是提醒此方法可能会抛出这些异常 调用此方法的时候需要处理这些异常
    public void test(int a, int b) throws ArithmeticException{

        if (b==0){
            throw new ArithmeticException(); // 主动抛出异常 一般在方法内使用 关键字 throw 异常对象
            // 主动抛出异常所在代码块的同层次后面不能再有后续代码 会报错
        }
        System.out.println("主动抛出异常后,后面代码不会再运行");// 主动抛出异常后程序终止 非同层次的后续代码也不会再运行
    }

}

/*
算数异常
finally
Error
finally
java.lang.ArithmeticException
java.lang.ArithmeticException
	at com.kimtanyo.exception.Test.test(Test.java:50)
	at com.kimtanyo.exception.Test.main(Test.java:31)
Exception in thread "main" java.lang.ArithmeticException
	at com.kimtanyo.exception.Test.test(Test.java:50)
	at com.kimtanyo.exception.Test.main(Test.java:38)
*/
package com.kimtanyo.exception;

public class Test2 {
    public static void main(String[] args) {
        int a = 1;
        int b = 0;

        // 选中包裹代码后 快捷键 Ctrl + Alt + T
        try {
            System.out.println(a/b);
        } catch (Exception e) {
            e.printStackTrace(); // 打印栈异常错误信息 会显示更深层的调用信息 没有try catch的时候正常编译如果报错 会自动输出
            System.out.println(e); // 普通打印直接错误信息
            System.exit(1);  // 终止程序 参数status为0表示正常退出 一般用在if else里面 参数staus非零表示非正常退出 一般用在try catch里面
        } finally {
        }
    }
}


/*
java.lang.ArithmeticException: / by zero
	at com.kimtanyo.exception.Test2.main(Test2.java:10)
java.lang.ArithmeticException: / by zero
*/

自定义异常及经验小结

声明自定义异常

在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。

  • 所有异常都必须是 Throwable 的子类。
  • 如果希望写一个检查性异常类,则需要继承 Exception 类。
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。

可以像下面这样定义自己的异常类:

class MyException extends Exception{
}

只继承Exception 类来创建的异常类是检查性异常类。

下面的 InsufficientFundsException 类是用户定义的异常类,它继承自 Exception。

一个异常类和其它任何类一样,包含有变量和方法。

使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常。用户自定义异常类,只需继承Exception类即可。

在程序中使用自定义异常类,大体可分为以下几个步骤:

  1. 创建自定义异常类。

  2. 在方法中通过throw关键字抛出异常对象。

  3. 如果在当前抛出异常的方法中处理异常,可以使用try-catch语句捕获并处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作。

  4. 在出现异常方法的调用者中捕获并处理异常。

package com.kimtanyo.exception.demo02;

public class MyException extends Exception{
    // 双击shift 可以打开Java类搜索栏 输入ArrayIndexOutOfBoundsException
    // 传递数字>10 抛出异常
    private int detail;

    public MyException(int detail) {
        this.detail = detail;
    }

    // toString() 这里重写了Object类的toString()方法
    // 通常 toString()方法 返回一个以文本方式表示此对象的字符串
    // System.out.println(x); 如果 x 不是String类型 那么就会自动调用x的toString()方法
    // Object类的toString()方法 返回的字符串是 "类名@HashCode"
    // 这里重写成打印异常信息

    @Override
    public String toString() {
        return "MyException{" +
                "detail=" + detail +
                '}';
    }
}
-------------------------------------------
package com.kimtanyo.exception.demo02;

public class Test {

    // 可能会存在异常的方法

    static void test(int a) throws MyException {
        System.out.println("传递的参数为:" + a);
        if (a>10){
            throw new MyException(a); // try catch 和 throws 二选一 要么方法内捕获区域中抛出 要么方法声明处抛出
        }

        System.out.println("如果抛出了异常不会继续运行至此处");
    }

    public static void main(String[] args) {
        try {
            test(11);
        } catch (MyException e) {
            //此处可以增加一些处理异常的代码而不是简单打印异常输出
            System.out.println("MyException=>" + e);
        }
    }


}

实际应用中的经验总结

  • 处理运行时异常时,采用逻辑去合理规避同时辅助 try-catch 处理
  • 在多重catch块后面,可以加一个catch(Exception)来处理可能会被遗漏的异常
  • 对于不确定的代码,也可以加上try-catch,处理潜在的异常
  • 尽量去处理异常,切忌只是简单地调用printStack Trace()去打印输出
  • 具体如何处理异常,要根据不同的业务需求和异常类型去决定
  • 尽量添加finally语句块去释放占用的资源