面向对象进阶

Java面向对象进阶特性

第三课:JAVA面向对象进阶

在上周我们已经了解了java面向对象的基础部分,面向对象的概念还有面向对象的三大特性(封装,继承,多态),相信大家应该已经比较熟悉了吧。Java的面向对象是很重要的一部分,这节课我们会在上次课的基础之上继续讲解Java面向对象的进阶部分。

抽象

抽象类

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是__抽象类__。

简而言之,就是抽象类不能__实例化__为一个对象。

由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。

父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。

在 Java 语言中使用 abstract 关键字来定义抽象类。比如我们把刚刚的Dog类改为抽象类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Dog {
private String name;
private int age;

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

public String getName() {
    return this.name;
}

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

public int getAge() {
    return this.age;
}

public void eat() {
    System.out.println("吃饭");
}
}

这个时候我们去创建一个Dog类的对象,就会报错。但是Dog的子类Husky和Corgi还是可以实例化。

抽象类总结规定

抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。

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

抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。

构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。

抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

抽象类与接口的比较

先来看看两者的区别:

抽象类特点

继承了抽象类的子类,要么对父类的抽象方法进行重写,要么自己也是抽象类

抽象类也可以拥有普通方法

抽象类不能创建对象

抽象类也有构造方法,但是是为了子类创建对象使用

接口的特点

接口是行为的抽象,是一种行为的规范,接口是like a 的关系;抽象是对类的抽象,是一种模板设计,抽象类是is a 的关系。

接口没有构造方法,而抽象类有构造方法,其方法一般给子类使用

接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。

抽象体现出了继承关系,继承只能单继承。接口体现出来了实现的关系,实现可以多实现。接口强调特定功能的实现,而抽象类强调所属关系。

接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。

简单来说,抽象类定义了他的子类的具体类别,而接口定义了他的实现类的行为。

抽象方法

如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为__抽象方法__。

abstract 关键字同样可以用来声明抽象方法,抽象方法只有声明,而没有具体的实现,如果父类中的方法还不能确定如何具体实现,那么这就应该写成一个抽象方法。(抽象方法在抽象类中只需要声明而不需要实现,即表示在这个类中有这么一个方法,但是他的具体实现由子类定义);

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

TIPS:抽象方法的访问权限不能为private

因为抽象方法一定要由子类实现,如果子类都访问不了,那么还有什么意义呢?所以说不能为私有。

final和static关键字

final关键字

final关键字我们在上节课提到过,这节课我们详细讲讲。

final的意思是最终,不可改变。从字面上大概也能猜出他的使用方法和效果。

1.当final修饰一个类时,表示这个类不能被继承,格式为:

1
2
3
public final class 类名称 {
//...
} 

2.当final修饰一个方法时,表示这个方法不能被覆盖重写,格式为:

1
2
3
修饰符 final 返回值类型 方法名称(参数列表) {
//方法体
}

注意:对于类和方法final和abstract关键字不能同时使用,因为矛盾。

3.当final用来修饰一个局部变量时,这个变量就不能进行更改。

对于基本类型来说,不可变说的是变量中的数据不可改变;

对于引用类型来说,不可变说的是变量中的地址值不可改变。

4.当final用来修饰一个成员变量时,这个变量同样也是不能进行更改。

由于成员变量有默认值,所以使用final之后就必须手动赋值,不会再给默认值了。

对于final的成员变量,要么使用直接赋值,要么通过构造方法赋值,二选其一。

必须保障类当中的所有重载的构造方法,都最终会对final的成员变量赋值。

static关键字

static就是静态的意思,他主要有以下一些用法:

1.当static用来修饰一个成员变量,那么这个变量就不再属于某一个对象,而是属于这个类,多个对象共享一份数据

经典案例:生成对象个数计数器:

1
2
3
4
5
6
7
8
9
public class Count {
//静态的全局int类型用于统计该类被实例化的次数
public static int count = 0;

public Count() {
    count++;
    System.out.println("第" + count + "次创建对象!!!");
}
}

2.当static用来修饰成员方法,那么这个方法也同样不属于某一个对象,而是属于这个类

对于静态方法来说,可以通过对象名来调用,也可以通过类名调用,更推荐用类名调用。

为什么 main 方法是静态的?

static是静态修饰符,被他修饰的方法我们称之为静态方法,静态方法有一个特点,那就是静态方法独立于该类的任何对象,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。而对于main方法来说,他的调用过程是经历了类加载、链接和初始化的。但是并没有被实例化过,这时候如果想要调用一个类中的方法。那么这个方法必须是静态方法,否则是无法调用的。(简言之,在程序刚刚启动时还不存在任何的对象,此时静态的main方法将执行并构造程序所需要的对象)

注意事项:

(1)静态方法不能直接访问非静态属性和方法。

在程序运行的过程中,static修饰的静态变量和全局变量都是储存在一个叫全局数据区的地方。也并不是说全局变量在定义时加了static关键字才是静态存储,不加static就是动态存储,不是的。不管加不加static,全局变量都是存储在静态存储区的,都是在编译时分配存储空间的,两者只是作用域不同,全局变量默认具有外部链接性,作用域是整个工程,全局静态变量的作用域仅限本文件,不能在其他文件中引用

(2)在静态方法中不能使用this关键字。

this代表当前对象,通过谁调用的方法,谁即为当前对象,但是静态方法并不会属于某个对象,而是属于整个类。

3.静态代码块

静态代码块直接写在类当中:

1
2
3
4
5
6
public class 类名称 {
//静态代码块    
static {
    //静态代码块内容
}
}

第一次执行本类时,静态代码块唯一执行一次。

4.使用static静态关键字的好处

不需要创建具体的对象,便可以直接调用类当中的静态方法。

在类中修饰变量时,可以实现同一个类当中的所有对象共享一份数据。

被static修饰的部分只在类第一次加载时被初始化一次,可以提高程序性能。

Lamabda表达式

Lambda表达式是在java8所新增的新特性。

lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。 Lambda 表达式可以看作是一个匿名函数。(这个其实用的也不多,在以后学习Stream流的时候用的多一点,现在稍作了解即可)

基本语法:

1
2
(参数) -> 表达式 
(参数) -> {表达式}

可以将上述表达式看成一个完整的函数,只是隐藏了修饰符,返回值类型和方法名称:

1
2
3
/*修饰符 返回值类型 方法名称*/(参数)/*->*/ {
表达式
}

一部分函数在调用时要传入的参数是一个函数式接口(一种特殊的接口,有且只有一个抽象方法),这种时候就可以用Lmabda表达式进行简写。

内部类

如果一个事物内部含有另一个事物,那么这就是一个类内部包含另一个类。

如同身体和心脏的关系。

成员内部类

1
2
3
4
5
6
修饰符 class 外部类名称 {
  修饰符 class 成员内部类名称 {
 //...
   }
   //...
}

注意:内用外,随意访问;外用内,一定要有内部类对象

成员内部类的使用:

1.间接方式:在外部类的方法中,使用内部类,然后main只是调用外部类方法。

2.直接方式:

1
2
类名称 对象名 = new 类名称();
外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();

如果出现重名现象,那么格式是:外部类名称.this.外部类成员变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Outer {
int num=10;//外部类成员变量
public class Inner {
    int num=20;//内部类成员变量
    public void methodInner() {
        int num=30;//内部类方法的局部变量
        System.out.println(num);//局部变量,就近原则
        System.out.println(this.num);//内部类的成员变量
        System.out.println(Outer.this.num);//外部类的成员变量
}
  }
}

局部内部类

如果一个类是定义在一个方法内部的,那么这就是一个局部内部类。

“局部”,只有当前所属的方法才能使用它,出了这个方法外面就不能用了

定义格式:

1
2
3
4
5
6
7
修饰符 class 外部类名称 {
修饰符 返回值类型 外部类方法名称(参数列表) {
   class 局部内部类名称 {
   //....
   }
}   
}

注意事项:

局部内部类,如果希望访问所在方法的局部变量,那么这个局部变量必须是有效final的。

备注:从Java 8+开始,只要局部变量事实不变,那么final关键字可以省略。

原因:

new 出来的对象在堆内存中。

局部变量是跟着方法走,在栈内存中。

方法运行结束后,立即出栈,局部变量就会消失。

但是new出来的对象会在堆内存中持续存在,直到垃圾回收消失。

匿名内部类

如果接口的实现类(或者是父类的子类)只需要使用唯一的一次,那么这种情况下就可以省略该类的定义,而改为使用匿名内部类,直接new接口或父类

匿名内部类的定义格式:

1
2
3
接口名称 对象名 =new 接口名称() {
@Override//覆盖重写所有抽象方法
};

new代表创建对象的动作

接口名称就是匿名内部类需要实现哪个接口,也可以是抽象父类

{……}这才是匿名内部类的内容。

匿名内部类所需要注意的事项

匿名内部类中不能存在任何静态成员或方法

匿名内部类是没有构造方法的,因为它没有类名

与局部内部相同匿名内部类也可以引用局部变量。此变量也必须声明为 final(局部变量与匿名内部类的生命周期不同)(从Java 8+开始,只要局部变量事实不变,那么final关键字可以省略)

作业

复习前两节课的内容,写一下课件中提到的代码(无需提交)