Java基础笔记(2)-面向对象

面向对象基础

1.包package

应用场景

  • 区分相同名称的类
  • 当类很多的时候,可以很好地管理类
  • 控制访问范围

基本语法

1
package xxx.yyy
  • package:关键字,表示打包
  • 后面加包的名称

包的本质

  • 创建不同的文件夹来保存类文件

包的使用

  • 需要先在当前src中new一个package,然后在package中新建class。实际上也是对应不同的文件夹
  • 如果要使用不同包的类,使用import指定引入对应的包里面的类
  • 也可以不使用import,直接在使用类的时候带上包名

包的命名规范

  • 一般是小写字母+小圆点
  • 一般是公司名.项目名.业务模块名

使用细节

  • package的作用是声明当前类所在的包,需要放在类的最上面,一个类最多只有一句package
  • import指令放在package的下面,在类定义的前面,没有顺序要求
  • 从编译器的角度来看,嵌套的包之间没有任何关系,每一个包都是独立的类集合

常用的包

包名 说明
java.lang.* lang包是基本包,默认引入,不需要再引入
java.util.* util包,系统提供的工具包,工具类,使用Scanner
java.net.* 网络包,网络开发
java.awt.* Java的界面开发,GUI

2.类与对象的快速入门

快速使用

1
2
3
4
5
6
7
8
class Cat{
String name;
int age;
String color;
}

//使用
myCat = new Cat();

Java对象内存布局:

  • 真正的对象存放在堆当中

  • 实例在栈空间中对应一个堆空间的地址,堆空间中对应存放了属性的值

  • 如果默认赋值,仍然是引用赋值(地址复制)

成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person{
//成员属性
String name;
int age;

//成员方法
public void speak(int n){
System.out.println("我是一个好人");
System.out.println(n);
}
//public:访问修饰符(public、protected、private、默认)
//后面的与C++中类似
}

方法调用方式:

  1. 当程序执行到方法时,就会开辟一个独立的栈空间
  2. 当方法执行完毕,或执行到return语句时,就会返回
  3. 返回到调用方法的地方
  4. 返回后继续执行方法后面的代码

注意事项:

  • 可以重载(overload)【函数签名:函数名称+参数类型+参数个数】

  • 可变参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public int calculate(int... nums){
    int res = 0;
    for (int i = 0; i < nums.length; i++) {
    res += nums[i];
    }
    return res;
    }

    // 基本语法:访问修饰符 返回类型 方法名(数据类型...形参名)
    // 可以接收多个参数
    // 使用可变参数的时候,可以将nums看作一个数组来使用
    • 可变参数可以和普通类型的参数一起放在形参列表,但是必须保证可变参数在最后

    • 一个形参列表中只能有一个可变参数

  • 作用域:全局变量有默认值、局部变量没有默认值、全局变量可以加修饰符、局部变量不可以加修饰符

  • 同样具有this关键字

3.构造方法(构造器)

构造方法完成初始化

1
2
3
4
Person(my_name, my_age){
this.name = my_name;
this.age = my_age;
}
  • 构造器的修饰符可以默认,也可以是public、protected,不能是private
  • 没有返回值
  • 方法名和类名一致
  • 系统自动调用
  • 构造方法可以进行重载
  • 同样有默认的无参构造器,如果定义了自己的构造器,则默认的构造器会被覆盖,就不能使用默认的无参构造器了,如果要使用,需要显式定义
  • 在构造器方法中可以调用其他构造器方法,使用this(参数列表)
    • 这个this()必须在构造器中的第一行,所以最多只能有一个

4.访问修饰符

提供四种访问修饰符,用于控制方法和属性的访问权限(范围)

  • public:对外公开

  • protected:对子类和同一个包中的类公开

  • 默认,无修饰符号:对同一个包中的类公开(有时候子类可能也在同一个包中)

  • private:只有类本身可以访问,不对外公开

修饰符 本类 同一个包 子类 不同包
public
protected ×
默认 × ×
private × × ×

使用细节:

  • 修饰符可以用来修饰类中的属性,成员方法以及类本身

  • 对于类本身来说,只能使用两种 默认和public,并且遵循上述访问权限的特点

5.静态变量和类方法

类变量(静态变量)

对于一个类来说,静态变量是所有类的实例所共享的,使用static来修饰

1
2
3
4
class Child{
private String name;
public static int count;//被Child的所有实例所共享
}
  1. 访问修饰符的规则与前面相同

  2. 实例和类名都可以对静态属性进行访问,建议使用类名来进行访问

  3. 静态变量的初值

    • 可以在类中直接赋值

    • 可以使用static代码块进行初值定义

    • 如果都没有,则是默认值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Person {
    //方法1,在类中直接赋值,id = 1
    public static int id = 1;

    //方法2,在static代码块中赋初值,id = 2
    public static int id = 1;
    static {
    id = 1;
    }

    //方法3,不写,为默认值 id = 0
    public static int id;
    }

类方法(静态方法)

1
访问修饰符 static 数据返回类型 方法名(){}
  1. 当方法中不涉及到任何和对象实例相关的成员,不生成实例也可以使用,则可以将方法设计成静态方法,提高开发效率
  2. 在实际设计的时候,往往将一些通用方法设计成静态方法,例如打印数组,排序等方法(工具类)
  3. 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区
  4. 类方法中没有this的参数,普通方法中隐含着this的参数
  5. 实例和类名都可以调用类方法
  6. 类方法中不允许使用和对象有关的关键字,比如thissuper
  7. 类方法中只能访问静态变量或静态方法
  8. 普通成员方法,既可以访问普通变量或方法,又可以访问静态变量或方法(遵守访问权限)

main方法

main方法的形式:

1
public static void main(String[] args){}

理解:

  1. main()方法是由Java虚拟机来进行调用的,所以该方法的访问权限必须pubilc

  2. Java虚拟机在执行main方法的时候不需要创建对象,所以是类方法static

  3. 接收String类型的数组参数,该数组中保存执行Java命令的时候传递给所运行的类的参数

  4. 在main方法中,我们可以直接调用main方法所在类的静态方法或静态属性;但是不能直接访问该类中的非静态成员,必须创建该类的一个实例对象之后才能通过这个对象去访问类中的非静态成员

IDEA main函数动态传值:Program arguments

工厂方法

静态方法的另外一种常见用途就是工厂方法,使用静态工厂方法来构造对象。构造对象可以通过构造器来完成,而工厂方法可以提供更多选择,更多类型的构造方式。

6.代码块

代码块:又称为初始化块,是类的一部分,类似于方法但是没有方法名,没有返回也没有参数,只有方法体。代码块不需要通过对象和类显式调用,而是在加载类,或创建对象的时候隐式调用

1
[修饰符]{代码块体};

细节:

  1. 修饰符可选,但是只能写static
  2. 有static的为静态代码块、没有static的为普通代码块
  3. 静态代码块只会在类加载的时候调用一次(对类进行初始化
  4. 普通代码块会在每次创建对象的时候都进行调用,在仅加载的时候不会被调用
  5. 静态代码块只能调用静态成员,普通代码块可以调用任意成员
  6. 代码块的调用顺序优先于构造器
  7. 构造器的最前面其实是隐含了super()和调用普通代码块
  8. 分号可写可省略

类被加载的时机:

  1. 创建对象实例的时候(new)
  2. 创建子类对象实例,父类也会被加载
  3. 使用类的静态成员的时候

创建一个对象的时候,在一个类中的执行情况

  1. 调用静态代码块和静态属性初始化(两种初始化调用的优先级相同,按照执行顺序调用)
  2. 调用普通代码块和普通属性初始化(两种初始化调用的优先级相同,按照执行顺序调用)
  3. 调用构造器

创建一个子类对象的时候,类中的执行情况:

(父类加载、子类加载、父类创建、子类创建)

  1. 父类的静态代码块和静态属性
  2. 子类的静态代码块和静态属性
  3. 父类的普通代码块和普通属性
  4. 父类的构造方法
  5. 子类的普通代码块和普通属性
  6. 子类的构造方法

7.JAR文件

在将应用程序打包的时候,我们希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构。Java归档文件(JAR)就是为此目的而设计的。一个JAR文件可以包含类文件,也可以包含诸如图像、声音等其他类型的文件。JAR文件采用ZIP压缩格式。

面向对象特性

1.封装

封装(encapsulation):黑盒,内部细节隐藏。对数据进行验证,保证安全合理

封装实现的步骤:

  1. 将属性进行私有化private,不能直接修改属性
  2. 提供一个公共的public的set方法,进行对属性的设置赋值
  3. 提供一个公共的public的get方法,进行对属性的值获取

2.继承

继承(extends)的必要性:提高代码复用,扩展性

继承的基本语法extends

1
2
3
class 子类 extends 父类{
...
}
  • 子类会自动拥有父类定义的属性和方法
  • 父类又可以叫做 超类、基类
  • 子类又可以叫做 派生类

继承的细节:

  1. 子类继承了所有的属性和方法,但是私有属性和方法不能在子类中直接访问(可以通过间接方法public访问)
  2. 子类必须调用父类的构造器,完成父类的初始化
  3. 当创建子类对象的时候,不管使用子类的哪一个构造器,默认情况下都会先调用父类的无参构造器
    • 默认有一个super(),默认调用父类的无参构造器
  4. 如果父类没有提供无参构造器,则必须在子类的构造器中使用super关键字来指定使用父类的哪一个构造器来完成对父类的初始化,否则编译报错
    • super(参数列表)即表示调用父类对应的构造器
    • super在使用的时候,必须要放在构造器的第一行
    • super()this()都必须要放在构造器的第一行,所以这两个方法不能存在于同一个构造器内
  5. java所有类都是Object类的子类,Object类是所有类的基类
  6. 父类构造器的调用(super)不限于直接父类,可以一直向上追溯直到Object类(顶级父类)
  7. 子类最多只能继承一个父类(直接继承),即Java中是单继承的机制
    • 如果想让A类继承B类和C类,构造继承链条A->B->C
  8. 不能滥用继承,子类和父类之间应该要满足IsA的逻辑关系

继承的本质

当子类对象创建好了之后,其实在内存中会保存一种子类和父类之间的查找关系。子类对象创建的时候,首先会从Object加载开始加载该子类的所有父类信息,然后子类对应的堆内存中保存了自己所有父类的变量空间,即使父类之间存在相同属性,也不会进行空间的覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
//继承关系:Son -> Father -> GrandPa
public GrandPa{
String name = "GrandPa";
String hobby = "traval";
}
public Father{
String name = "Father";
int age = 40;
}
public Son{
String name = "Son";
double height = 1.70;
}

使用son.来进行属性的访问过程:

  1. 首先看子类是否具有该属性
  2. 如果子类具有该属性并且可以访问(访问修饰符的作用),那么就返回该信息,如果不能访问就直接报错而不会再往下面找
  3. 如果子类没有这个属性,就看父类有没有,按照规则逐级向上,直到找到一个或者哪里都找不到报错

super关键字:super代表父类的引用,可以用于访问父类的属性、方法和构造器

基本语法:super.属性 super.方法 super(参数列表)

  1. 可以访问父类的属性和方法(受访问控制符限制)
  2. 可以访问父类的构造器

使用细节:

  1. 调用父类构造器的好处,达到分工明确的效果(父类属性父类初始化、子类属性子类初始化)
  2. 当子类和父类存在同名的成员的时候,为了访问父类的成员必须使用super
    • 如果不存在同名,则super、this和直接访问是一样的效果
  3. super的访问不限于直接父类,如果爷爷类和本类中具有同名的成员,也可以使用super去访问爷爷类中的成员;如果多个父类(上级类)中都有同名的成员,使用super访问按照就近原则进行

super和this的比较:

区别点 this super
访问属性 访问本类中的属性,如果没有再从父类中进行查找 直接访问父类中的属性
调用方法 访问本类中的方法,如果没有再从父类中进行查找 直接访问父类中的方法
调用构造器 调用本类构造器,必须放在构造器的首行 调用父类构造器,必须放在子类构造器的首行
特殊 表示当前对象 用于子类中访问父类对象

方法重写/覆盖(override):

简单来说,方法重写就是子类有一个方法和父类的某个方法的名称、返回类型和参数等都一样,我们就说子类的这个方法覆盖了父类的那个方法(但是并不是完全覆盖,父类的方法并没有消失,通过super仍然可以访问到

  1. 子类的方法的参数、方法名称要和父类的参数,方法名称完全一样
  2. 子类的返回类型和父类的返回类型一样,或者是父类的返回类型的子类(eg:父类的返回类型是Object、子类的返回类型是String,这种情况也可以,即返回类型也要是子类
  3. 访问控制符不需要完全相同,但是子类方法不能缩小父类方法的访问权限

3.多态

多态(polymorphic):代码复用,代码维护

  • 方法或者对象具有多种形态,称之为多态。多态是建立在封装和继承的基础之上的
  • 多态的具体体现
    • 方法的多态:重写和重载体现多态
    • 对象的多态:多态的核心

对象的多态:可以让父类的引用指向子类的对象

  1. 一个对象的编译类型运行类型可以不一致
  2. 编译类型在定义对象的时候就去确定了,不能改变
  3. 运行类型是可以变化的(体现多态)
  4. 编译类型看定义的时候等号的左边,运行类型看等号的右边
1
2
Animal animal = new Dog();//animal的编译类型是Animal,运行类型是Dog
animal = new Cat();//animal的运行类型变成了Cat,但是编译类型仍然是Animal

多态的细节:

  1. 多态的前提是:两个类之间是存在继承关系的

  2. 多态的向上转型:

    • 本质:父类的引用指向了子类的对象(子类向上转型成为父类,不限于直接父类)

    • 语法:父类类型 引用名称 = new 子类类型();

    • 特点:

      • 编译类型看左边,运行类型看右边
      • 可以调用父类中的所有成员(遵循访问权限)
      • 不能调用子类中的特有成员(在编译的时候能够调用哪些成员是看编译类型)
      • 最终执行效果以子类中(运行类型)的实现为主
  3. 多态的向下转型:

    • 本质:当前对象向下转型成为与运行类型相同的类型

    • 语法:子类类型 引用名称 = (子类类型) 父类引用名称;

    • 特点:只能强转父类的引用,不能强转父类的对象;

      ​ 要求父类的引用必须指向的是当前目标类型的对象;(运行类型和目标类型相同)

      ​ 可以调用子类类型中的所有成员;

  4. 直接调用属性的时候,看的是编译类型中的实现

  5. instanceOf操作符:用于判断对象的类型(运行类型)是否为某某类型或者某某类型的子类型

    • 在将父类强制转换成子类之前,应该使用instancof进行检查

动态绑定机制

  • 当调用对象方法的时候,具有动态绑定机制,该方法会和该对象的内存地址/运行类型绑定

    • 如果是private、static、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这称为静态绑定
  • 当调用对象属性的时候,没有动态绑定机制,哪里声明,哪里使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class A {
    int i = 10;
    public int getI() {
    return i;
    }
    public int sum1() {
    return i + 10;
    }
    public int sum2() {
    return getI() + 10;
    }
    }
    class B extends A {
    int i = 20;
    public int getI() {
    return i;
    }
    }
  • 如果使用b去调用sum1()方法,里面的i使用的是父类里的i

  • 如果使用b去调用sum2()方法,里面的getI()方法使用的是子类里面的getI()方法

面向对象补充

1.final关键字

final:修饰关键字,可以用来修饰类、属性、方法和局部变量

使用到final的一些情况:

  1. 当不希望类被继承的时候,使用final修饰
  2. 当不希望父类的某个方法被子类覆盖或者重写(override)的时候,使用final修饰
  3. 当不希望类的某个属性的值被修改,使用final修饰
  4. 当不希望某个局部变量的值被修改,使用final修饰

final使用细节:

  1. final修饰的属性又称为常量,用大写+下划线来命名
  2. final修饰的属性在定义的时候必须赋初值,并且不能修改,赋初值的位置有
    • 定义时
    • 在构造器中
    • 在代码块中
  3. 如果final修饰的属性是静态的,则赋初值的位置只能是定义时或者静态代码块中
  4. 如果类已经被声明了final,其中的方法就没必要再用final了
  5. final不能修饰构造方法
  6. final和static往往搭配使用,效率更高,调用该属性的时候不会使得类加载。(底层编译器实现了优化)
  7. 包装类(Boolean,Integer...)和String类都是final的

2.抽象类

应用场景引出:当父类的某些方法需要声明但是又不确定该如何实现,可以将其声明为抽象方法,此时这个类就是抽象类

抽象方法的声明:(抽象方法没有实现,即没有方法体,空的也不行)

1
2
3
abstract class xxx{
访问修饰符 abstract 返回类型 方法名称();
}
  • 使用了abstract声明了抽象方法,此时类也需要使用abstract来声明

使用细节:

  1. 抽象类不能被实例化(new)

  2. 可以定义一个抽象类的对象变量

    举例来说,Person是一个抽象类,Student继承了Person并实现了其中的抽象方法

    1
    2
    new Person(); // 错误
    Person p = new Student(); // 正确
  3. 抽象类不一定要包含abstract方法

  4. abstract只能用来修饰类或者方法

  5. 抽象类的本质还是类,可以拥有任何应有的成员

  6. 如果一个类继承了抽象类,则必须实现抽象类中的所有抽象方法,或者自己也声明成abstract类

  7. 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的

3.Object类的详解

Object类是Java中所有类的始祖,在Java中每个类都扩展了Object,但是不需要显式地写出。如果没有明确地指出父类,那么就认为Object是这个类的超类

Object方法摘要:

方法名 方法说明
clone() 创建并返回此对象的一个副本
equals() 指示其他某个对象是否与此对象相等
finalize() 当GC确定不存在对该对象的更多引用的时候,由对象的垃圾回收机制调用
getClass() 返回此Object的运行时类
hashCode() 返回该对象的哈希码
notify() 唤醒在此对象监视器上等待的单个线程
notifyAll() 唤醒在此对象监视器上等待的所有线程
toString() 返回该对象的字符串表示
wait() 线程等待

equals方法:是Object类中的方法,只能判断引用类型

默认判断的是地址是否相等,在子类中一般会对该方法进行重写,用于判断内容是否相等。例如Integer

==和equals的对比:

==是一个比较运算符:

  1. ==既可以判断基本类型,又可以判断引用类型
  2. 如果是判断基本类型,那么就比较是否相等
  3. 如果是判断引用类型,那么就判断地址的值是否相等,即判断是否是一个同一个对象

Java语言规范要求equals方法具有下面的特性:

  1. 自反性:对于任何非空应用x,x.equals(x)应该返回true
  2. 对称性:对于任何引用x和y,应该有y.equals(x) \(\Leftrightarrow\) x.equals(y)
  3. 传递性:对于任何引用x,y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也要返回true
  4. 一致性:如果x和y引用的对象没有发生变化,反复调用x.equal(y)应该返回相同的结果
  5. 对于任意非空引用x,x.equals(null)应该返回false

在重写Object中的equals方法的时候,一个常见的错误是定义的形参类型不是Object,这样的话是没有进行方法的重写,而是定义了一个完全无关的方法。因此在重写的时候可以增加注解@Override来进行检查

hashCode方法:返回该对象的哈希码

  1. 用于提高具有哈希结构的容器的效率
  2. 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的
  3. 两个引用,如果指向的是不同对象,则哈希值是不一样的(不严谨,可能存在冲突)
  4. 哈希值主要与地址有关,但是不能完全等价于地址
  5. 有必要的时候也可以重写hashCode方法
  6. 如果重写了equals方法,那么也需要重写hashCode方法

toString方法:返回该对象的字符串表示

  1. 默认返回全类名@哈希值的十六进制表示(全类名 = 包名 + 类名)
  2. 子类往往重写toString方法,用于返回对象的属性信息(一般情况下是这样,并且IDEA中有生成的快捷键)
  3. 重写toString方法,打印对象或者拼接对象的时候,都会自动调用该对象的toString形式
  4. 当直接输出一个对象的时候,toString方法会被默认调用

finalize方法:垃圾回收的时候会被调用(已经被废弃

  1. 当对象被回收的时候,系统会自动调用该对象的finalize方法。
  2. 子类可以重写该方法,做一些释放资源的操作
  3. 当某个对象没有任何引用的时候,JVM就认为这个对象是垃圾对象,就会使用垃圾回收机制来销毁该对象。在销毁该对象之前,会调用该对象的finalize方法
  4. 垃圾回收机制的调用是由系统来决定的,也可以通过System.gc()来主动触发垃圾回收机制(也不一定绝对成功)

4.包装类与自动装箱

包装类(Wrapper):所有的基本类型都有一个与之对应的类,这些类被成为包装器。这些包装器类是不可变的,一旦构造了包装器,就不允许更改其中的值。同时包装器类还是final,因此不能派生它们的子类

基本数据类型 包装类
boolean Boolean
char Character
byte Byte
short Short
int Integer
long Long
float Float
double Double

装箱和拆箱:包装类和基本数据类型的相互转换

装箱:基本数据类型 -> 包装类型

拆箱:包装类型 -> 基本数据类型

  • JDK 5 之前采用手动装箱和拆箱的方式
  • JDK 5 以后(含JDK 5)采用自动装箱和拆箱的方式
  • 自动装箱底层调用了valueOf方法
  • 装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码的时候会插入必要的方法调用,虚拟机只是执行这些字节码

手动装箱和拆箱:

1
2
3
4
5
6
//手动装箱
int n = 100;
Integer integer = new Integer(n);
Integer integer = Integer.valueOf(n);
//手动拆箱
int i = integer.intValue();

自动装箱和拆箱:

1
2
3
4
5
//自动装箱
int n = 200;
Integer integer = n;//底层使用的仍然是ValueOf方法
//自动拆箱
int i = integer;//底层仍然是.intValue方法

valueOf的源码会进行判断,举Integer.valueOf为例。如果传入的整数在-128到127范围内,会直接返回Integer对象,否则再才会new一个新对象。(下为源码)

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)//-128~127
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

包装类和String的相互转换

包装类 -> String

1
2
3
4
Integer i = 100;
String str = i + "";//方式一
String str = i.toString();//方式二
String str = String.valueOf(i);//方式三

String -> 包装类

1
2
3
String str = "12345";
Integer i = Integer.parseInt(str);//方式一
Integer i = new Integer(str);//方式二

包装类的常用方法

方法名(部分为属性名) 功能
MIN_VALUE 返回最大值
MAX_VALUE 返回最小值
isDigt() 判断是否是数字
isLetter() 判断是否是字母
isUpperCase() 判断是否是大写
isLowerCase() 判断是否是小写
isWhitespace() 判断是否是空格
toUpperCase() 转成大写
toLowerCase() 转成小写

5.枚举类

枚举(enumeration、enum):枚举是一组常量的集合,枚举属于一种特殊的类,里面包含一组特定的有限的对象。实现枚举的两种方式有自定义枚举类实现,以及使用关键字enum

我们可以自定义实现一个类来达到枚举类的效果:

  1. 将构造器私有化,防止外部直接调用new
  2. 删除set相关方法(防止属性被修改,保证只读)
  3. 在类内部直接创建固定的对象(枚举变量名通常大写,遵循常量的命名规范)
  4. 优化,可以加入final修饰符(final与static的编译器联合优化)

当然在Java中有枚举类的实现,使用enum关键字即可。枚举类的实例都事先定义了,如下面的枚举类只有四个实例。因此在比较两个枚举类的值的时候,并不需要调用equals,直接使用==即可。

1
2
3
4
5
6
7
enum Season {
SPRING("春天"),SUMMER("夏天"),AUTUMN("秋天"),WINATER("冬天");

private String name;

private Season(String name){this.name = name;}
}
  1. 使用关键字enum替代class
  2. 使用常量名(实参列表)代替手动new的固定对象

枚举类的注意事项:

  1. 当使用enum的时候开发枚举类的时候,会默认继承一个名称为Enum的类,而且枚举类是一个final类
  2. 如果使用无参构造器,那么括号可以省略
  3. 如果有多个常量对象,使用,间隔
  4. 如果使用enum来实现枚举,要求将常量对象的定义放在最前面
  5. 使用了enum关键字后,就不能再继承其他类了(因为已经默认继承了Enum类,而Java是单继承机制)
  6. 但是使用了enum关键字后,还是可以实现接口
  7. 枚举的构造器总是私有的,可以省略private修饰符

所有的枚举类型都是Enum类的子类,因此继承了这个类的许多方法。

成员函数 函数说明
toString Enum类中已经重写了,返回当前对象名。子类可以重写该方法
name 返回当前对象名(常量名),子类不能重写(final)
ordinal 返回当前对象的位置号,默认从0开始
values 返回当前枚举类中的所有常量(不是常量名)
valueOf 将字符串转换成枚举对象,要求字符串必须为已有的常量名,否则报异常
compareTo 比较两个枚举常量(实际比较的就是位置号,本身编号 - 传入对象的编号 )

6.反射

反射机制

一个快速入门小demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// className = "cat", methodName= "hi" 均为字符串类型

// 1.加载类,返回Class类型的对象
Class cls = Class.forName(className);

// 2.通过cls得到所加载的对象实例
Object o = cls.newInstance();

// 3.通过cls得到对应加载类的Method对象,即在反射中,可以把方法视为对象
Method method1 = cls.getMethod(methodName);

// 4.通过method1来调用方法,通过方法对象来实现方法的调用
// 传统方法是 对象.方法() 在反射机制中是 方法.invoke(对象)
method1.invoke(o);

// 得到类中的字段
Field nameField = cls.getField("name"); // 不能得到私有字段
String nameValue = nameField.get(o); // 获得name字段的值
  1. 反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法等),并能够操作对象的属性及方法。反射在设计模式和框架底层中都会用到

  2. 加载完类之后,在堆中就产生了一个Class类型的对象(这是一个对象,这个对象的类型是Class,一个类只有一个Class对象)。这个对象中包含了类的完整结构信息,通过这个对象就能够的得到类的结构。这个Class对象就像一面镜子,透过这个镜子能够看到类的结构

  3. 通过反射机制可以完成

    • 在运行时判断任意一个对象所属的类
    • 在运行时构造任意一个类的对象
    • 在运行时得到任意一个类所具有的成员变量和方法
    • 在运行时调用任意一个对象的成员变量和方法
    • 生成动态代理
反射相关的主要类
java.lang.Class 代表一个类,Class对象表示某个类加载后在堆中的对象
java.lang.reflect.Method 代表类的方法,Method对象表示某个类的对象
java.lang.reflect.Field 代表类的成员变量,Field对象表示某个类的成员变量
java.lang.reflect.Constructor 代表类的构造方法,Constructor对象表示某个类的构造器

Class类

在程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。这个辛纳希会跟踪每个对象会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确的方法。

我们可以通过一个特殊的Java类来访问这些信息,即Class类。

基本介绍

  1. Class也是类,因此也继承Object类
  2. Class类对象不是new出来的,而是系统创建的
  3. 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
  4. 每个类的实例都会记得自己是由哪一个Class实例所生成的
  5. 通过Class对象可以完整地得到一个类的完成结构(提供了一系列API)
  6. Class对象是存放在堆中的
Class类的常用方法
static Class forName(String name) 返回指定类名name的Class对象
Objecgt newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所对应的实体(Class、Field、Method...)名称
Class getSuperClass() 返回当前Class对象的父类的Class对象
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor [] getConstructors() 返回一个包含Constructor对象的数组
Field [] getDeclaredFields() 返回Field对象数组
Method getMethod(String name, Class... paramTypes) 返回一个Method对象,此对象的形参类型为paramType

获取Class对象的方法:

  1. Class.forName():已知一个类的全类名,可以通过Class类的静态方法forName获取
    • 多用于配置文件加载类
  2. 类.class:已知具体的类,通过该类的class获取。该方式最安全可靠,程序性能最高
    • 多用于参数传递,比如通过反射得到对应的构造器对象
  3. 对象.getClass():在前面说的这是运行类型。已知对象实例,可以通过getClass方法获取
  4. 从类加载器获得Class对象
  5. 基本数据类型(int、double...):Class cls = int.class
  6. 基本类型对应的包装类:Class cls = 包装类.TYPE

反射应用

反射机制中一个重要的内容就是检查类的结构。

java.lang.reflect包中有三个类Filed、Method和Constructor分别用于描述类的字段、方法和构造器。这三个类都有一个叫做getName的方法,用来返回对应的名称。每个类都同时具有更多方法,分别返回对应字段、方法或构造器的描述信息。

Java安全机制允许查看一个对象有哪些字段。但是除非是拥有访问权限,否则不允许读写那些字段的值。反射机制的默认行为受限于Java的访问控制,不过可以调用Field、Method或者Constructor对象的setAccessible方法覆盖Java的访问控制。调用setAccessible(true)则可以绕过。

通过反射创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 首先获得User对应的Class对象
Class<?> userClass = ClassforName("User");

// 调用pubilc,默认的无参构造器创建User类
Object o = userClass.newInstance();

// 调用pubilc的单参构造器创建User类
Constructor<?> constructor = userClass.getConstructor(String.class);
Object o1 = constructor.newInstance("hahaha");

// 调用private的双参构造器创建User类
Constructor<?> constructor1 = userClass.getConstructor(String.class, String.class);
constructor1.setAccessible(true); // 暴破,使用反射可以访问private的构造器
Object o2 = constructor1.newInstance("hahaha", "hahaha");

通过反射访问类中的成员

1
2
3
4
5
6
7
8
9
10
11
Class<?> stuClass = Class.forName("Student");
Object o = stuClass.newInstance();

// 使用反射得到pubilc属性对象
Field age = stuClass.getField("age");
age.set(o, 88);

// 使用反射得到privare属性对象
Field name = stuClass.getDeclaredField("name");
name.setAccessible(true);
name.set(o,"hahaha");

通过反射访问类中的方法

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> stuClass = Class.forName("Student");
Object o = stuClass.newInstance();

// 调用pubilc的方法
Method hi = stuClass.getMethod("hi", String.class);
hi.invoke(o, "hahaha");

// 调用private的方法
Method say = stuClass.getDeclaredMethod("say", String.class);
say.setAccessible(true);
say.invoke(o, "lalala");
// 在反射中,如果方法有返回值,则统一返回Object,但运行类型与定义的返回类型一致

面向对象高级

1.接口

一个快速入门的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//接口
public interface USB{
//规定接口的相关方法
pubilc void start();
pubilc void end();
}
//类 实现 接口
public class Phone implements USB {
@Override
public void start(){};
@override
public void end(){};
}
public class Camara implements USB {
@Override
public void start(){};
@override
public void end(){};
}
//使用接口
public class Computer{
public void work(USB usb){
usb.start();
usb.end();
}
}
  • Phone类和Camara需要实现USB接口中的所有声明的方法

  • 在Computer中规定使用接口的相关方法

接口(interface):接口是用来指定规范的。接口就是给出一些没有实现的方法,将他们封装在一起,某个类要使用的时候,再根据情况把这些方法写出来。

1
2
3
4
5
6
7
8
9
interface 接口名{
//接口属性
//接口方法
}
class 类名 implements 接口{
//自己的属性
//自己的方法
//必须实现的接口的抽象方法
}

注意事项:

  1. 在JDK 7.0 之前,接口中的所有方法都没有方法体
  2. JDK 8.0 之后,接口类还可以有静态方法,默认方法(接口中可以有方法的具体实现)
    • 默认方法需要使用default关键字
    • 静态方法需要使用static关键字
    • 其他的就是抽象方法
  3. 在接口中,abstract方法可以省略关键字

接口的使用细节:

  1. 接口不能被实例化

  2. 接口中所有的方法默认是public方法,接口中的抽象方法可以不使用abstract来修饰

  3. 接口方法声明的时候可以不指定为pubilc,默认是public。但是在实现接口的时候,必须把方法声明为pubilc

  4. 一个普通类实现接口,就必须将接口的所有方法都实现

  5. 抽象类实现接口,可以不用实现接口的方法

  6. 一个类同时可以实现多个接口(implements多个)

  7. 接口中的属性只能是final的,而且是pubilc static final,并且必须在定义的时候进行初始化

    • 在定义的时候可以只写int a = 1,但是等价于public static final int a = 1
    • 接口中的属性都是静态的
  8. 接口中属性的访问形式:接口名称.属性名(static)

  9. 一个接口不能继承其他的类,但是可以继承多个别的接口(implements多个)

  10. 接口的修饰符只能是public和默认,与类的修饰符相同

  11. 接口是对Java单继承机制的一种补充

    • 当子类继承了父类,就自动拥有了父类的功能
    • 如果子类需要扩展功能,可以通过实现接口的方式来扩展
  12. 解决默认方法冲突(如果是抽象方法,则不存在冲突,因此一定需要实现)

    • 接口和父类冲突:父类优先
    • 接口之间冲突:由程序员在本类中实现,来消除二义性

2.lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或者多次。

lambda表达式的语法如下:

1
2
3
(params) -> expression
(params) -> statement
(params) -> { statements }
  • 即时没有参数,也要提供空括号
  • 如果可以推导出lambda表达式参数类型,那么可以忽略其类型
  • 如果只有一个参数,并且参数类型可以推导得出,那么可以省略小括号
  • 如果是使用{ }的代码块,则需要包含显式的return语句

另一个概念是函数式接口(functional interface)。对于只有一个抽象方法的接口,当我们需要这种接口的对象时,就可以提供一个lambda表达式。这样的接口被称为函数式接口。

方法引用:

有时候lambda表达式只涉及一个方法的调用,我们可以使用方法引用。方法引用指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法,调用给定的方法。

1
2
3
event -> System.out.println(event)
// 变为方法引用
System.out::println

例如,如果我们需要对字符串排序,并且不考虑字母的大小写,可以使用下面的方式

1
Arrays.sort(strings, String::compareToIgnoreCase)

我们需要使用::运算符来分隔方法名与对象名或者类名

lambda表达式可以捕获外围作用域中变量的值,但是只能引用值不会改变的变量,不能在lambda表达式中改变外部变量。

在Java中,lambda表达式就是闭包。

3.内部类

内部类:一个类的内部又嵌套了另一个类结构。被嵌套的类称为内部类,嵌套其他类的类称为外部类。内部类是类的第五大成员(属性、方法、构造器、代码块)。内部类的最大特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。

基本语法:

1
2
3
4
class Outer{//外部类
class Inner{//内部类
}
}

内部类的分类:

  • 如果内部类定义在外部类的局部位置上(比如成员方法中)
    • 局部内部类(有类名)
    • 匿名内部类(没有类名)
  • 如果内部类定义在外部类的成员位置上
    • 成员内部类(没有static修饰)
    • 静态内部类(有static修饰)

1.局部内部类

局部内部类:定义在外部类的局部位置,比如说方法、代码块中,并且有类名

说明:

  1. 可以直接访问外部类的所有成员,包括私有成员
  2. 局部内部类相当于一个局部变量。不能使用修饰符,但是可以使用final修饰
  3. 作用域仅仅在定义它的方法或者代码块当中
  4. 在方法中调用内部类的方法需要先实例化(new),再调用
  5. 如果外部类和局部内部类的成员重名的时候,默认遵循就近原则。如果想访问外部类的成员,可以使用外部类名.this.成员来进行访问

2.匿名内部类

匿名内部类:定义在外部类的局部位置,比如说方法、代码块中,并且没有类名

本质是类;是一个内部类;该类没有名字(没有用户命名,但是有系统命名 外部类$id);同时还是一个对象

说明:

  1. 匿名内部类的基本语法

    1
    编译类型 xxx = new 类或接口(参数列表){类体};
  2. 使用背景:针对那些只使用一次后面不再使用的类,使用匿名类来简化开发

  3. 这里的类或接口表示生成的匿名内部类扩展了类或者实现了接口

  4. 编译类型为对应的编译类型,运行类型为匿名内部类

  5. JDK底层在创建匿名内部类之后,立即就创建了实例,并且返回对应在栈中的地址

  6. 匿名内部类不能有构造器,但是可以提供一个对象初始化块

  7. 匿名内部类使用一次,就不再使用

  8. 大括号里的类体是匿名内部类的全部定义

  9. 参数列表中指定的是构造器所用的参数,正常的new中同样有参数列表

  10. 匿名内部类,即有类的特征,又有对象的特征

  11. 可以直接访问外部类的所有成员,包括私有成员

  12. 局部内部类相当于一个局部变量。不能使用修饰符,但是可以使用final修饰

  13. 作用域仅仅在定义它的方法或者代码块当中

  14. 如果外部类和匿名内部类的成员重名的时候,默认遵循就近原则。如果想访问外部类的成员,可以使用外部类名.this.成员来进行访问

经典使用场景:将匿名内部类直接当作实参进行传递

3.成员内部类

成员内部类:定义在外部类的成员位置,并且没有static修饰

说明:

  1. 可以直接访问外部类的所有成员,包括私有的

  2. 可以添加任意访问修饰符(public、protected、默认、private),它实际上也是一个成员

  3. 作用域和外部类的其他成员一样,为整个类体

  4. 成员内部类中不能定义static性质的成员属性和方法

  5. 成员内部类访问外部类:直接访问

  6. 外部类访问内部类:先创建对象,再访问

  7. 其他外部类访问内部类:找到外部类,再找到内部类,创建对象再访问

    • 方式一:相当于new Inner()是Outer的一个成员

      1
      Outer.Inner x = Outer.new Inner()
    • 方式二:在Outer中编写一个方法返回内部类Inner

      1
      2
      3
      public Inner getInner(){
      return new Inner();
      }
  8. 如果外部类和成员内部类的成员重名的时候,默认遵循就近原则。如果想访问外部类的成员,可以使用外部类名.this.成员来进行访问

4.静态内部类

静态内部类:定义在外部类的成员位置,并且有static修饰

说明:

  1. 可以直接访问外部类的所有静态成员,包括私有的。但是不能直接访问非静态成员

  2. 可以添加任意访问修饰符(public、protected、默认、private),它实际上也是一个成员

  3. 作用域和外部类的其他成员一样,为整个类体

  4. 静态内部类可以有静态字段和方法

  5. 静态内部类访问外部类:直接访问

  6. 外部类访问内部类:先创建对象,再访问

  7. 其他外部类访问内部类:

    • 方式一:可以通过类名直接访问得到

      1
      Outer.Inner xxx = new Outer.Inner();
    • 方式二:在Outer中编写一个方法返回内部类Inner

  8. 如果外部类和静态内部类的成员重名的时候,默认遵循就近原则。如果想访问外部类的成员,可以使用外部类名.成员来进行访问(这里不需要this,因为只能访问静态成员)


Java基础笔记(2)-面向对象
http://example.com/2022/09/02/Java基础笔记-2-面向对象/
作者
EverNorif
发布于
2022年9月2日
许可协议