文章目录
前言
系列文章比较适合非科班的同学, 由浅及深, 循序渐进, 从基础开始走上大数据之路
让我们一起努力, 达成目标, 实现理想
最后恳请留下你的小❤, 点赞给我一个小小的鼓励吧
一、final关键字
1.1 概述
学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。那么我们能不能随意的继承API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了final
关键字,用于修饰不可改变内容。
final: 最终的,不可改变的。可以用于修饰类、方法和变量.
- 类:被修饰的类,不能被继承。
- 方法:被修饰的方法,不能被重写。
- 变量:被修饰的变量,不能被重新赋值。
1.2 final详细解释
1.被修饰的类,不能被继承
格式:
final class 类名{}
查询API发现像 public final class String
、public final class Math
、public final class Scanner
等,很多我们学习过的类,都是被final修饰的,目的就是供我们直接使用,而不让我们随意修改其内容(API帮助文档之后会详细展开介绍具体的类的)。
2.被修饰的方法,不能被重写
格式:
修饰符列表 final 返回值类型 方法名(参数列表){
方法体
}
重写final修饰的方法,编译时就会报错。
3.被修饰的变量,不能被重新赋值
- <1>成员变量
final修饰成员变量必须在创建对象之前赋值,有两种方式初始化方式:
// 显示初始化
public class User {
final String NAME = "张三"; //直接在定义的时候给定一个默认值
private int age;
}
//构造方法初始化
public class User {
final String NAME ;
private int age;
public User(){
this.NAME = "张三";
}
public User(String name, int age) {
this.NAME = name;
this.age = age;
}
}
被final修饰的常量名称,有特殊的命名规范,所有字母都要大写
- <2>局部变量 —— 基本数据类型
基本类型的局部变量,被final修饰后,只能赋值一次,不能再做更改。
举例:
public class Test01 {
public static void main(String[] args) {
final int a;
a = 10;
// a = 20; //在编译的过程中就会报错
System.out.println(a);
}
}
思考:
以下两种写法有问题吗?
public static void main(String[] args) {
final int a;
for (int i = 0; i < 10; i++) {
a = i;
System.out.println(a);
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
final int a = i;
System.out.println(a);
}
}
根据 final
的定义,上面的写法报错!第二种写法为什么通过编译呢?因为每次循环,都是一次新的不能改变的变量a。
- <3>局部变量 —— 引用数据类型
引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改。
举例:
public static void main(String[] args) {
final User u = new User();
u = new User(); //不能赋新的地址值
u.setName("张三"); //可以修改
}
二、接口
2.1 概述
接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,接口中所有方法都是抽象方法(JDK 7及以前,在JDK8中新出了默认方法和静态方法)。
接口只描述所应该具备的方法,并没有具体实现,是没有方法体的。具体的实现由接口的实现类(相当于接口的子类)来完成。这样将功能的定义与实现分离,优化了程序设计,降低了程序的 耦合性,实现了解耦合。
2.2 接口的定义
接口的定义,它与定义类方式相似,但是使用 interface
关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。
格式:
public interface 接口名{
常量;
抽象方法
}
使用interface代替了原来的class,其他步骤与定义类相同:
- 接口中的方法均为公共访问的抽象方法
- 接口中无法定义普通的成员变量
注意:
- 一般的public class 类名.java在编译之后的class文件名类似Test.class
- 接口public interface 接口名.java在编译之后的class文件名类似Test$1.class
2.3 类实现接口
实现的概述
类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements
关键字。
实现格式:
public class 类名 implements 接口名{
//重写接口中的所有抽象方法(强制)
}
在类实现接口后,该类就会将接口中的抽象方法继承过来,此时该类需要重写该抽象方法,完成具体的逻辑。
- 接口中定义功能,当需要具有该功能时,可以让类实现该接口,只声明了应该具备该方法,是功能的声明。
- 在具体实现类中重写方法,实现功能,是方法的具体实现。
举例:
接口
public interface Test02 {
// public static final String NAME = "张三";
// public abstract void move();
// public abstract String name();
// 由于接口中只存在常量和抽象方法,所以可以简写为:
String NAME = "张三";
void move();
String name(String name);
}
实现类
public class Test03 implements Test02 {
@Override
public void move() {
System.out.println("在移动");
}
@Override
public String name(String name) {
return name;
}
//测试
public static void main(String[] args) {
Test04 a = new Test04();
System.out.println(a.name(NAME));
a.move();
}
}
2.4 接口中成员的特点
接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static final修饰。
public interface Test04 {
int NUM0 ; // 错误,必须赋值
int NUM1 =10; // 正确 , 省去了默认修饰符 public static final
public static final int NUM2= 100; // 正确 , 完整写法
}
解释一下:为什么成员变量一定要用static final修饰?
- static:因为接口是允许多实现的。如果一个类实现了两个接口,且两个接口都具有相同名字的变量,此时这个变量可以被实现类使用,那么如果不是static的,这个变量来自哪一个接口就会产生歧义,所以实现类使用接口中的变量必须通过接口名指定,也就只能定为static的。
- final:既然必须是static修饰的,那么所有子类共享。而接口是一种抽象, 所以一个子类修改了值会影响到其他所有子类,因此就不应该允许子类修改这个值,所以也必须定义为final。
接口中可以定义方法,方法也有固定的修饰符,public abstract
public interface Test05 {
void method(); //正确 没写public abstract java自动补全
public abstract void method2(); //正确 完成写法
public void method3(); //正确 没写abstract java自动补齐
private void method4(); //错误
public void method5(){} //错误 接口中都是抽象方法
}
接口没有构造方法,不能直接创建对象
public interface Test05{
public LiveAble(){} //错误 接口没有构造方法
}
2.5 接口的特点(注意事项)
- 类与类是继承关系,类与接口是实现关系,一个类可以在继承一个类的同时实现多个接口
public interface JieKou1
{
void show1();
void show2();
}
public interface JieKou2
{
void show3();
void show4();
}
public abstract class Fu{
public abstract void showFu1();
public abstract void showFu2();
}
//有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次
public class Zi extends Fu implements JieKou1, JieKou2 // 多实现,同时实现多个接口。
{
public void show1(){}
public void show2(){}
public void show3(){}
public void show4(){}
public abstract void showFu1(){}
public abstract void showFu2(){}
}
注意:一个类继承一个父类的同时实现一个或者多个接口,要重写所有的抽象方法
如果两个接口中有重名的抽象方法,那么我们就重写一个
- 接口与接口的关系继承关系,可以单继承,也可以多继承
一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用 extends
关键字,子接口继承父接口的方法
public interface A {
void show1();
}
public interface B {
void show2();
}
public interface C extends A,B{
void show3();
}
public class D implements A,B,C{
void show1(){}
void show2(){}
void show3(){}
}
2.6 接口的好处
- 接口解决了java单继承的局限性,提高了程序的扩展性
我们都知道java中类是单继承的,不可以多继承,为什么呢?因为多继承可能造成安全隐患,多继承时,当多个父类中有相同功能时,子类调用会产生不确定性。其实核心原因就是在于多继承父类中功能有主体,而导致调用运行时,不确定运行哪个主体内容。如:
public class A{
public void show(){
System.out.println("A");
}
}
public class B{
public void show(){
System.out.println("B");
}
}
//假设可以多继承的话
public class C extends A,B{}
//当创建C的对象调用show方法时根本不能确定调用的A的show还是B的show方法
但是接口就可以多实现,因为接口中的方法都没有方法体,方法体的具体内容是由子类来实现的。所以为了避免出现这种不安全的隐患java是单继承的,但是单继承是有局限性的,比如一个类已经有父类,还想为这个类添加一些功能时,是没有办法再继承一个的类的,但是这时可以通过实现接口的方式来添加。
- 接口的出现降低了耦合性,即设备与设备之间实现了解耦。
耦合代表着事物与事物之间的的紧密程度,解耦合就是降低事物之间的联系.打个比方你买了一台电脑,假如鼠标是焊死在电脑上的我们就说耦合性比较高,耦合度太高就会有一些问题,比如鼠标坏了是没办法更换的。但是我们现在买的笔记本上面都有USB接口,这个USB接口就帮我们降低了电脑和鼠标之间的耦合性,如果鼠标坏了,拔下来换一个,并且也提高了程序的扩展性,这个USB接口不止可以插鼠标还可以插键盘,插任何符合USB接口规则的东西。
java中类与类继承的耦合性就是挺高的。如果父类中定义了很多方法并且都给出了实现,子类继承后,可以直接使用这些功能,但是如果子类不继承这个父类了,子类中什么功能都没有了。但是接口就不会,实现类必须实现接口的功能,接口给出方法声明,实现类进行具体实现,即使实现类不实现接口了,也不会造成太的的影响。
public class Fu {
public void show1() {}
public void show2() {}
}
//子类继承父类 可以直接使用父类中的方法 ,如果子类不继承父类了 子类中什么方法都没有了
public class Zi extends Fu {
}
public interface A {
void show1();
void show2();
}
//B实现A 必须实现所有方法 如果B不实现A了 也没有太大的影响 还可以正常使用
public class B implements A {
void show1() {}
void show2() {}
}
2.7 抽象类和接口的区别
抽象类:
接口:
由图可以看出两者一些相同点:
- 都位于继承的顶端,用于被其他类实现或继承;
- 都不能直接实例化对象;
- 都包含抽象方法,其子类都必须覆写这些抽象方法;
区别:
- 抽象类为部分方法提供实现,避免子类重复实现这些方法,提高代码重用性;接口只能包含抽象方法;
- 一个类只能继承一个直接父类(可能是抽象类),却可以实现多个接口;(接口弥补了Java的单继承)
- 抽象类为继承体系中的共性内容,接口为继承体系中的扩展功能
2.8 总结抽象类
成员区别
- 抽象类:变量,常量;有构造方法;有抽象方法,也有非抽象方法
- 接口:常量;抽象方法
关系区别
- 类与类:继承,单继承
- 类与接口:实现,可以单实现,也可以多实现
- 接口与接口:继承,单继承,多继承
设计理念区别
- 抽象类:对类抽象,包括属性、行为
- 接口:对行为抽象,主要是‘功能’
三、多态
3.1 概述
多态是继封装、继承之后,面向对象的第三大特性。
现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态。
Java作为面向对象的语言,同样可以描述一个事物的多种形态。如Student类继承了Person类,一个Student的对象便既是Student,又是Person。
Java中多态的代码体现在一个子类对象(实现类对象)既可以给这个子类(实现类对象)引用变量赋值,又可以给这个子类(实现类对象)的父类(接口)变量赋值。
如Student类可以为Person类的子类。那么一个Student对象既可以赋值给一个Student类型的引用,也可以赋值给一个Person类型的引用。
class Student extens Person{}
Student s = new Student(); //创建一个学生对象,把学生对象赋值给学生类型s
Person p = new Student(); //创建一个学生对象,把学生对象赋值给人类型p
最终多态体现为父类引用变量可以指向子类对象
前提条件【重点】
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
3.2 多态的定义与使用格式
多态的定义格式:就是父类的引用变量指向子类对象
父类类型 变量名 = new 子类对象;
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
普通类多态定义的格式
//父类 变量名 = new 子类();
class Fu {}
class Zi extends Fu {}
//类的多态使用
Fu f = new Zi();
抽象类多态定义的格式
//抽象类 变量名 = new 抽象类子类();
abstract class Fu {
public abstract void method();
}
class Zi extends Fu {
public void method(){
System.out.println(“重写父类抽象方法”);
}
}
//类的多态使用
Fu fu= new Zi();
接口多态定义的格式
//接口 变量名 = new 接口实现类();
interface Fu {
public abstract void method();
}
class Zi implements Fu {
public void method(){
System.out.println(“重写接口抽象方法”);
}
}
//接口的多态使用
Fu fu = new Zi();
需要注意的是,同一个父类的方法会被不同的子类重写。在调用方法时,调用的为各个子类重写后的方法。如:
Person p1 = new Student();
Person p2 = new Teacher();
p1.work(); //p1会调用Student类中重写的work方法
p2.work(); //p2会调用Teacher类中重写的work方法
当变量名指向不同的子类对象时,由于每个子类重写父类方法的内容不同,所以会调用不同的方法。
3.3 多态-成员的特点
多态调用变量时:
编译时期:到父类中找有没有这个变量,如果有编译成功,如果没有编译失败.
运行时期:访问的是父类成员变量的值
简单记:编译看=号左边,运行看=左边
public class Fu {
int num = 4;
}
public class Zi extends Fu {
int num = 5;
int a = 10;
}
public class Demo {
public static void main(String[] args) {
Fu fz = new Zi();
System.out.println(fz.num);//4
//System.out.println(fz.a);// 父类中没有这个变量 编译失败
}
}
多态调用方法时:
编译时期:到父类中去找这个方法,如果有编译成功,如果没有编译失败
运行时期:运行的是子类重写后的方法.
简而言之:编译看左边,运行看右边。
public class Fu{
public void show1(){
System.out.println("fu");
}
}
public class Zi{
//子类重写父类方法
public void show1(){
System.out.println("Zi");
}
//子类特有方法
public void show2(){
System.out.println("子类特有方法");
}
}
public class Demo {
public static void main(String[] args) {
Fu fz = new Zi();
fz.show1();//打印 Zi
//fz.show2();//父类中没有这个方法编译失败
}
}
3.4 多态的好处
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。
举例:
定义父类:
public abstract class Animal {
public abstract void eat();
}
定义子类:
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Cat c = new Cat();
Dog d = new Dog();
// 调用showCatEat
showCatEat(c);
// 调用showDogEat
showDogEat(d);
/*
以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代
而执行效果一致
*/
showAnimalEat(c);
showAnimalEat(d);
}
public static void showCatEat (Cat c){
c.eat();
}
public static void showDogEat (Dog d){
d.eat();
}
public static void showAnimalEat (Animal a){
a.eat();
}
}
由于多态特性的支持,showAnimalEat方法的Animal类型,是Cat和Dog的父类类型,父类类型接收子类对象,当然可以把Cat对象和Dog对象,传递给方法。
当eat方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上两方法。
不仅仅是替代,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用showAnimalEat都可以完成。
所以,多态的好处体现在可以使程序编写的更简单,并有良好的扩展。
3.5 多态的转型
多态的转型分为向上转型与向下转型两种:
- 向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的,称作自动类型转换。
当父类引用指向一个子类对象时,便是向上转型。
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
- 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的,称作强制类型转换。
一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
子类类型 变量名 = (子类类型) 父类变量名;
如:Cat c = (Cat) a;
为什么要转型?
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,无法调用子类独有,父类中没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
转型演示,代码如下:
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void watchHouse() {
System.out.println("看家");
}
}
测试类:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
}
}
转型的异常
转型的过程中,一不小心就会遇到这样的问题,请看如下代码:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】
}
}
这段代码可以通过编译,但是运行时,却报出了 ClassCastException
,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。
为了避免ClassCastException的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验,格式如下:
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
所以,转换前,我们最好先做一个判断,举例:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}
学习到这里,面向对象的三大特征结束!
总结下封装、继承、多态的作用:
- 封装:把对象的属性与方法的实现细节隐藏,仅对外提供一些公共的访问方式
- 继承:子类会自动拥有父类所有可继承的属性和方法。
- 多态:配合继承与方法重写提高了代码的复用性与扩展性;如果没有方法重写,则多态同样没有意义。
四、综合案例
定义笔记本类,具备开机,关机和使用USB设备的功能。具体是什么USB设备,笔记本并不关心,只要符合USB规格的设备都可以。鼠标和键盘要想能在电脑上使用,那么鼠标和键盘也必须遵守USB规范,不然鼠标和键盘的生产出来无法使用;
进行描述笔记本类,实现笔记本使用USB鼠标、USB键盘
-
USB接口,包含开启功能、关闭功能
-
笔记本类,包含运行功能、关机功能、使用USB设备功能
-
鼠标类,要符合USB接口
-
键盘类,要符合USB接口
参考:
import java.util.ArrayList;
class Laptop{
ArrayList<USB> usbs;
public void turnOn(){
System.out.println("笔记本开机");
}
public void turnOff(){
System.out.println("笔记本关机");
}
public void load(ArrayList<USB> usbs){
this.usbs = usbs;
}
public void check(){
for (int i = 0; i < usbs.size(); i++) {
usbs.get(i).open();
}
}
}
interface USB{
public void open();
public void close();
}
class Mouse implements USB{
@Override
public void open() {
System.out.println("鼠标插上");
}
@Override
public void close() {
System.out.println("拔出鼠标");
}
}
class KeyBoard implements USB{
@Override
public void open() {
System.out.println("键盘插上");
}
@Override
public void close() {
System.out.println("拔出键盘");
}
}
//测试类
public class Test06 {
public static void main(String[] args) {
Laptop lenovo = new Laptop();
ArrayList<USB> arr = new ArrayList<>();
arr.addAll(Arrays.asList(new Mouse(),new KeyBoard()));
lenovo.turnOn();
lenovo.load(arr);
lenovo.check();
lenovo.turnOff();
}
}
总结
左上角主页里面有所有系列文章喔!
消除贫穷的最好办法就是和我一起学大数据,加油,奥利给!
看到这里点个赞吧!