【Java SE】抽象类、接口、final、代码块、单例模式、枚举
写在前面
- 关于文章
本篇文章主要学习了JavaSE的基础进阶内容的
抽象类,接口、代码块、final、单例、枚举
等等。在这里学习这块基础知识时,这些知识偏向于语法,难度不大,偏向于记忆
,学起来相对比较轻松,但是树高千尺,营养还在根部。基础不牢,地动山摇。
所以还是有必要把基础知识学扎实。这篇文章是在老师提供的学习资料的基础上整理完善出来的。
- 我的站点
- 联系我
🐧:
2410685763
WeChat:
Super_Baby_00
公众号:
编程那点事儿
抽象类
应用场景
父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定)
,父类完全只需要提供一个没有方法体的方法签名即可,具体实现交给子类自己去实现。我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类
。
- 抽象方法 : 只有方法签名没有方法体,必须用abstract修饰。
- 抽象类:包含抽象方法的类。本身也要用abstract修饰。
abstract使用
abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。
抽象方法
使用abstract
关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。
定义格式:
修饰符 abstract 返回值类型 方法名 (参数列表);
代码举例:
public abstract void run();
抽象类
如果一个类包含抽象方法,那么该类必须是抽象类。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。
定义格式:
abstract class 类名字 {
}
// 示例
public abstract class Animal {
public abstract void run();
}
抽象类的使用案例
// 父类,抽象类
abstract class Employee {
private String id;
private String name;
private double salary;
public Employee() {
}
public Employee(String id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
// 抽象方法
// 抽象方法必须要放在抽象类中
abstract public void work();
}
// 定义一个子类继承抽象类
class Manager extends Employee {
public Manager() {
}
public Manager(String id, String name, double salary) {
super(id, name, salary);
}
// 2.重写父类的抽象方法
@Override
public void work() {
System.out.println("管理其他人");
}
}
// 定义一个子类继承抽象类
class Cook extends Employee {
public Cook() {
}
public Cook(String id, String name, double salary) {
super(id, name, salary);
}
@Override
public void work() {
System.out.println("厨师炒菜多加点盐...");
}
}
// 测试类
public class Demo10 {
public static void main(String[] args) {
// 创建抽象类,抽象类不能创建对象
// 假设抽象类让我们创建对象,里面的抽象方法没有方法体,无法执行.所以不让我们创建对象
// Employee e = new Employee();
// e.work();
// 3.创建子类
Manager m = new Manager();
m.work();
Cook c = new Cook("ap002", "库克", 1);
c.work();
}
}
此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法
。
一个类继承了抽象类,必须重写完抽象类的全部抽象方法,否则这个类必须定义成抽象类。
因为拥有抽象方法的类必须定义成抽象类。
抽象类的特征
抽象类的特征总结起来可以说是 有得有失
有得:抽象类得到了拥有抽象方法的能力。
有失:抽象类失去了创建对象的能力。
其他成员(构造器,实例方法,静态方法等)抽象类都是具备的。
面试题:抽象类是否有构造器,是否可以创建对象,为什么?
答:抽象类作为类一定有构造器,而且必须有构造器。
提供给子类继承后调用父类构造器使用的。
抽象类虽然有构造器,但是抽象类绝对不能创建对象。
抽象类中可能存在抽象方法,抽象方法不能执行。
抽象在学术上本身意味着不能实例化。
抽象类的注意事项
- 抽象类
不能创建对象
,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
- 抽象类中,
可以有构造器,是供子类创建对象时,初始化父类成员使用的
。理解:
子类的构造方法中,有默认的super(),需要访问父类构造方法
。 - 抽象类中,
不一定包含抽象方法,但是有抽象方法的类必定是抽象类
。理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
- 抽象类的子类,
必须重写抽象父类中**所有的**抽象方法
,否则子类也必须定义成抽象类,编译无法通过而报错。理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
抽象类存在的意义是为了被子类继承,抽象类体现的是模板思想
。理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。
抽象类存在的意义
抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义,抽象类体现的是模板思想,模板是通用的东西抽象类中已经是具体的实现(抽象类中可以有成员变量和实现方法),而模板中不能决定的东西定义成抽象方法,让使用模板(继承抽象类的类)的类去重写抽象方法实现需求,这是典型的模板思想。
抽象类存在的意义有两点:
(1)被继承,抽象类就是为了被子类继承,否则抽象类将毫无意义。(核心意义)
(2)抽象类体现的是"模板思想":部分实现,部分抽象。(拓展)
-- 可以使用抽象类设计一个模板模式。
接口
概述
抽象类中可以用抽象方法,也可以有普通方法,以及构造器,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,接口中全部是抽象方法。(JDK8之前),接口同样是不能创建对象的
。
定义格式
//接口的定义格式:
修饰符 interface 接口名称{
// 抽象方法
}
// 修饰符:public|缺省
// 接口的声明:interface
// 接口名称:首字母大写,满足“驼峰模式”
接口成分的特点
在JDK8之前,接口中的成分包含:抽象方法和常量
抽象方法
注意:接口中的抽象方法默认会自动加上public abstract
修饰程序员无需自己手写!!
按照规范:以后接口中的抽象方法建议不要写上public abstract。因为没有必要啊,默认会加上。
常量
在接口中定义的成员变量默认会加上: public static final
修饰。也就是说在接口中定义的成员变量实际上是一个常量
。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量
。常量必须要给初始值
。常量命名规范建议字母全部大写,多个单词用下划线连接
。
案例
public interface InterF {
// 抽象方法!
// public abstract void run();
void run();
// public abstract String getName();
String getName();
// public abstract int add(int a , int b);
int add(int a , int b);
// 它的最终写法是:
// public static final int AGE = 12 ;
int AGE = 12; //常量
String SCHOOL_NAME = "黑马程序员";
}
基本的实现
类与接口的关系为实现关系
,即类实现接口
,该类可以称为接口的实现类
,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements
关键字。
实现接口的格式
/**接口的实现:
在Java中接口是被实现的,实现接口的类称为实现类。
实现类的格式:*/
[修饰符] class 类名 implements 接口1,接口2,接口3...{
}
从上面格式可以看出,接口是可以被多实现
的。
实现接口的要求和意义
- 必须重写实现的
全部接口中所有抽象方法
。 - 如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成
抽象类
。 - 意义:接口体现的是一种
规范
,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。
基本实现案例
假如我们定义一个运动员的接口(规范),代码如下:
/**
接口:接口体现的是规范。
* */
public interface SportMan {
void run(); // 抽象方法,跑步。
void law(); // 抽象方法,遵守法律。
String compittion(String project); // 抽象方法,比赛。
}
接下来定义一个乒乓球运动员类,实现接口,实现接口的实现类代码如下:
/**
* 接口的实现:
* 在Java中接口是被实现的,实现接口的类称为实现类。
* 实现类的格式:
* [修饰符] class 类名 implements 接口1,接口2,接口3...{
*
*
* }
* */
public class PingPongMan implements SportMan {
@Override
public void run() {
System.out.println("乒乓球运动员稍微跑一下!!");
}
@Override
public void law() {
System.out.println("乒乓球运动员守法!");
}
@Override
public String compittion(String project) {
return "参加"+project+"得金牌!";
}
}
多实现案例
类与接口之间的关系是多实现的,一个类可以同时实现多个接口
首先我们先定义两个接口,代码如下:
/** 法律规范:接口*/
public interface Law {
void rule();
}
/** 这一个运动员的规范:接口*/
public interface SportMan {
void run();
}
然后定义一个实现类:
/**
* Java中接口是可以被多实现的:
* 一个类可以实现多个接口: Law ,SportMan
*
* */
public class JumpMan implements Law ,SportMan {
@Override
public void rule() {
System.out.println("尊长守法");
}
@Override
public void run() {
System.out.println("训练跑步!");
}
}
从上面可以看出类与接口之间是可以多实现的,我们可以理解成实现多个规范,这是合理的。
接口与接口的多继承
Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口
。大家一定要注意:
类与接口是实现关系
接口与接口是继承关系
接口继承接口就是把其他接口的抽象方法与本接口进行了合并。
案例演示:
public interface Abc {
void go();
void test();
}
/** 法律规范:接口*/
public interface Law {
void rule();
void test();
}
/
* 总结:
* 接口与类之间是多实现的。
* 接口与接口之间是多继承的。
* */
public interface SportMan extends Law , Abc {
void run();
}
JDK 8之后的接口新增方法
从JDK 8开始之后,接口中不再只是抽象方法,接口还可以有默认方法
(也就是实例方法),和静态方法
了,还包含了私有实例方法和私有静态方法。
默认方法和静态方法
默认方法:使用 default
修饰,不可省略
,供子类调用或者子类重写
。
静态方法:使用 static
修饰,供接口直接调用
。
代码如下:
public interface InterFaceName {
// 必须用接口的实现类的对象来调用。
public default void method() {
// 执行语句
}
// 接口的静态方法必须用接口的类名本身来调用
public static void method2() {
// 执行语句
}
}
含有私有方法和私有静态方法
私有方法:使用 private
修饰,供接口中的默认方法或者静态方法调用
。
代码如下:
public interface InterFaceName {
// 只能在本接口中被其他的默认方法或者私有方法访问。
private void method() {
// 执行语句
}
}
新增方法的使用
默认方法和静态方法以及私有方法和私有静态方法,遵循面向对象的继承关系使用原则,实现类依然可以访问接口的非私有方法,对于接口中的非私有静态方法,可以直接通过接口名进行访问。
重写默认方法注意(了解):
- 子接口重写默认方法时,default关键字可以保留。
- 实现类重写默认方法时,default关键字不可以保留。
实现多个接口注意事项
多个接口同名静态方法
如果实现了多个接口,多个接口中存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法
。
public interface A {
public static void test(){
}
}
interface B {
public static void test(){
}
}
class C implements A , B{
public static void main(String[] args) {
A.test();
B.test();
// C.test(); // 编译出错
}
}
优先级的问题
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。代码如下:
定义接口:
interface A {
public default void methodA(){
System.out.println("AAAAAAAAAAAA");
}
}
定义父类:
class D {
public void methodA(){
System.out.println("DDDDDDDDDDDD");
}
}
定义子类:
class C extends D implements A {
// 未重写methodA方法
}
定义测试类:
public class Test {
public static void main(String[] args) {
C c = new C();
c.methodA();
}
}
输出结果:
DDDDDDDDDDDD
接口小结
- 接口中,
无法定义成员变量,但是可以定义常量,其值不可以改变
,默认使用public static final
修饰。 - 接口中的方法
全是抽象方法
,默认会自动加上public abstract
修饰 - JDK 8开始,接口不再纯洁,支持
静态方法
,默认方法
,私有方法
。 - 接口中,
没有构造器
,不能创建对象。 类与接口是多实现的
接口与接口是多继承的
- 接口体现的
规范
。
代码块
静态代码块
必须有static修饰,必须放在类下。与类一起加载执行
。
格式
static{
// 执行代码
}
特点:
- 每次
执行类,加载类
的时候都会先执行静态代码块一次。 - 静态代码块是
自动触发执行
的,只要程序启动静态代码块就会先执行一次。 - 作用:
在启动程序之前可以做资源的初始化,一般用于初始化静态资源
。
案例演示
public class DaimaKuaiDemo01 {
public static String sc_name ;
// 1.静态代码块
static {
// 初始化静态资源
sc_name = "黑马程序员!";
System.out.println("静态代码块执行!");
}
public static void main(String[] args) {
System.out.println("main方法执行");
System.out.println(sc_name);
}
}
实例代码块
没有static修饰,必须放在类下。与对象初始化一起加载
。
格式
{
// 执行代码
}
特点:
无static修饰。属于对象,与对象的创建一起执行的
。- 每次
调用构造器初始化对象
,实例代码块都要自动触发执行一次
。 - 实例代码块实际上是
提取到每一个构造器中去执行的
。 - 作用:实例代码块用于初始化对象的资源。
案例演示
public class DaimaKuaiDemo02 {
private String name ;
// 实例代码块。 无static修饰。
{
System.out.println("实例代码块执行");
name = "dl";
}
// 构造器
public DaimaKuaiDemo02(){
//System.out.println("实例代码块执行");
}
// 有参数构造器
public DaimaKuaiDemo02(String name){
//System.out.println("实例代码块执行");
}
public static void main(String[] args) {
// 匿名对象,创建出来没有给变量。
new DaimaKuaiDemo02();
new DaimaKuaiDemo02();
new DaimaKuaiDemo02("xulei");
}
}
// 输出三次:实例代码块执行
final关键字
概述
为了避免随意改写父类方法的情况,Java提供了final
关键字,用于修饰不可改变内容。
final: 不可改变,最终的含义。可以用于修饰类、方法和变量。
- 类:被修饰的类,
不能被继承
。 - 方法:被修饰的方法,
不能被重写
。 - 变量:被修饰的变量,
有且仅能被赋值一次
。
使用方式
修饰类
final修饰的类,不能被继承。
格式如下:
final class 类名 {
}
代码:
final class Fu {
}
// class Zi extends Fu {} // 报错,不能继承final的类
查询API发现像 public final class String
、public final class Math
、public final class Scanner
等,很多我们学习过的类,都是被final修饰的,目的就是供我们使用,而不让我们所以改变其内容。
修饰方法
final修饰的方法,不能被重写。
格式如下:
修饰符 final 返回值类型 方法名(参数列表){
//方法体
}
代码:
class Fu2 {
final public void show1() {
System.out.println("Fu2 show1");
}
public void show2() {
System.out.println("Fu2 show2");
}
}
class Zi2 extends Fu2 {
// @Override
// public void show1() {
// System.out.println("Zi2 show1");
// }
@Override
public void show2() {
System.out.println("Zi2 show2");
}
}
修饰变量-局部变量
- 局部变量——基本类型
基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改。代码如下:
public class FinalDemo1 {
public static void main(String[] args) {
// 声明变量,使用final修饰
final int a;
// 第一次赋值
a = 10;
// 第二次赋值
a = 20; // 报错,不可重新赋值
// 声明变量,直接赋值,使用final修饰
final int b = 10;
// 第二次赋值
b = 20; // 报错,不可重新赋值
}
}
思考,如下两种写法,哪种可以通过编译?
写法1:
final int c = 0;
for (int i = 0; i < 10; i++) {
c = i;
System.out.println(c);
}
写法2:
for (int i = 0; i < 10; i++) {
final int c = i;
System.out.println(c);
}
根据 final
的定义,写法1报错!写法2,为什么通过编译呢?因为每次循环,都是一次新的变量
c。这也是需要注意的地方。
修饰变量-实例成员变量
成员变量涉及到初始化的问题,初始化方式有显示初始化和构造器初始化,只能选择其中一个:
- 显示初始化(在定义成员变量的时候立马赋值);
public class Student {
final int num = 10;
}
- 构造器初始化(在构造器中赋值一次)。注意:每个构造器中都要赋值一次!
public class Student {
final int num = 10;
final int num2;
public Student() {
this.num2 = 20;
// this.num2 = 20;
}
public Student(String name) {
this.num2 = 20;
// this.num2 = 20;
}
}
被final修饰的常量名称,一般都有书写规范,所有字母都大写。
拓展:final和abstract的关系?
互斥关系,不能同时修饰类或者同时修饰方法!!
单例设计模式
单例设计模式的作用
单例模式,是一种常用的软件设计模式。通过单例模式可以保证系统中,应用该模式的这个类只有一个实例。即一个类只有一个对象实例。
单例设计模式实现步骤
- 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
- 在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型的成员变量。
- 定义一个静态方法返回这个唯一对象。
单例设计模式的类型
根据实例化对象的时机单例设计模式又分为以下两种:
- 饿汉单例设计模式
- 懒汉单例设计模式
饿汉单例设计模式
饿汉单例设计模式就是使用类的时候已经将对象创建完毕,不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故被称为“饿汉模式”。
代码如下:
public class Singleton {
// 1.将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
private Singleton() {}
// 2.在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型的成员变量。
private static final Singleton instance = new Singleton();
// 3.定义一个静态方法返回这个唯一对象。
public static Singleton getInstance() {
return instance;
}
}
懒汉单例设计模式
懒汉单例设计模式就是调用getInstance()方法时实例才被创建,先不急着实例化出对象,等要用的时候才例化出对象。不着急,故称为“懒汉模式”。
代码如下:
public class Singleton {
// 2.在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型的成员变量。
private static Singleton instance;
// 1.将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
private Singleton() {}
// 3.定义一个静态方法返回这个唯一对象。要用的时候才例化出对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
注意:懒汉单例设计模式在多线程环境下可能会实例化出多个对象,不能保证单例的状态。
枚举
不使用枚举存在的问题
假设我们要定义一个人类,人类中包含姓名和性别。通常会将性别定义成字符串类型,效果如下:
public class Person {
private String name;
private String sex;
public Person() {
}
public Person(String name, String sex) {
this.name = name;
this.sex = sex;
}
// 省略get/set/toString方法
}
public class Demo01 {
public static void main(String[] args) {
Person p1 = new Person("张三", "男");
Person p2 = new Person("张三", "abc"); // 因为性别是字符串,所以我们可以传入任意字符串
}
}
不使用枚举存在的问题:可以给性别传入任意的字符串,导致性别是非法的数据,不安全。
枚举的作用
枚举的作用:一个方法接收的参数是固定范围之内的时候,那么即可使用枚举
。
枚举的基本语法
概念
枚举是一种特殊类
。枚举是有固定实例个数的类型
,我们可以把枚举理解成有固定个数实例的多例模式。
定义格式
enum 枚举名 {
// 第一行都是罗列枚举实例,这些枚举实例直接写大写名字即可。
}
案例
- 定义枚举:BOY表示男,GIRL表示女
enum Sex {
BOY, GIRL; // 男,女
}
- Perosn中的性别有String类型改为Sex枚举类型
public class Person {
private String name;
private Sex sex;
public Person() {
}
public Person(String name, Sex sex) {
this.name = name;
this.sex = sex;
}
// 省略get/set/toString方法
}
- 使用是只能传入枚举中的固定值
public class Demo02 {
public static void main(String[] args) {
Person p1 = new Person("张三", Sex.BOY);
Person p2 = new Person("张三", Sex.GIRL);
Person p3 = new Person("张三", "abc");
}
}
枚举的其他内容
枚举的本质是一个类,我们刚才定义的Sex枚举最终效果如下:
enum Sex {
BOY, GIRL; // 男,女
}
// 枚举的本质是一个类,我们刚才定义的Sex枚举相当于下面的类
final class SEX extends java.lang.Enum<SEX> {
public static final SEX BOY = new SEX();
public static final SEX GIRL = new SEX();
public static SEX[] values();
public static SEX valueOf(java.lang.String);
static {};
}
枚举的本质是一个类,所以枚举中还可以有成员变量,成员方法等。
public enum Sex {
BOY(18), GIRL(16);
public int age;
Sex(int age) {
this.age = age;
}
public void showAge() {
System.out.println("年龄是: " + age);
}
}
public class Demo03 {
public static void main(String[] args) {
Person p1 = new Person("张三", Sex.BOY);
Person p2 = new Person("张三", Sex.GIRL);
Sex.BOY.showAge();
Sex.GIRL.showAge();
}
}
小结
枚举类在第一行罗列若干个枚举对象
。(多例)第一行都是常量
,存储的是枚举类的对象。枚举是不能在外部创建对象的,枚举的构造器默认是私有的
。- 枚举通常用于做信息的标志和分类。
微信关注