提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。
提示:以下是本篇文章正文内容,下面案例可供参考
一、包
包(package)是组织类的一种方式,使用包的目的是保证类的唯一性: 大白话讲:你代码中有一个Person类,你同学也写了一个Person类,如果将来你们代码需要一起用就会出现同名类而报错。
1.1导入包中的类
比如我们现在要得到一个时间戳:
import java.util.Date;
public class TestDemo {
public static void main(String[] args) {
Date data=new Date();
java.util.Date date=new java.util.Date();
System.out.println(data.getTime());
}
}
ps:import:“引入”,指引入类中需要的类 package:“包”,指类所在的包
我们需要用到java类库里的一些代码时,我们需要用import进行相应的导入,需要注意的是,我们import是导入一个具体的类而不能是导入一个具体的包,比如上面的得到时间戳
import java.util.Date;
import java.util;
关于import.util.*
*是一个通配符,它会导入一个包里面所有的类,那这个时候就有问题了:util下面有很多的类,那你import.util. *就一下子全部导入了嘛?
答案是否定的:java处理的时候,需要哪个类才会拿哪个类,这里要区别C语言的include<>,C中导入某个头文件就会把那个头文件的所有内容全部拿过来,java相对更细化一点。
比如上面写的import java.util.Date;我们用import java.util.*;代替
import java.util.*;
public class TestDemo {
public static void main(String[] args) {
Date data=new Date();
System.out.println(data.getTime());
}
}
我们只用了util里面Date的类,那么*也只会导入Date的类,而不是一股脑全拉过来。
我们实际工作或学习中还是建议导入具体的类,那个import java.xxx. *;范围还是太大了,有时候你把握不住。 举个栗子:
import java.util.*;
import java.sql.*;
public class TestDemo {
public static void main(String[] args) {
Date data=new Date();
}
}
你可能当时是想用util里面的Data,但是后面有些程序又需要用sql里面的一些类,你一股脑util和sql都是用*来导入,没有导入具体的类,问题就出现了。
util和sql里面都有Data这个类,那电脑怎么知道你要导入哪个包里面的Date啊,这里就会报错。
1.2静态导入
静态导入,工作学习都使用的很少,大家了解即可。 使用import static可以导入包中的静态方法和字段。
举例说明:我们平时经常会用到System.out.println();System是一个类,我们通过import static java.lang.System.*;导入这个类,然后这个类底下的静态方法就可以直接使用了,我们直接out.println()即可完成打印
import static java.lang.System.*;
public class TestDemo {
public static void main(String[] args) {
out.println("hello");
}
}
静态导入写出来的程序对理解程度要求高,且看起来稀奇古怪,我们了解即可,不需要深入理解。
1.3将类放到包中
基本规则: 1.在文件的最上方加一个package语句,指定该代码在哪个包中。 2.包名需要尽量指定成唯一的名字,通常会用公司的域名的颠倒形式(比如:你原先域名为www.bidu.com那么颠倒形式就是com.bidu.www) 3.包名要和代码路径相匹配,例如创建com.bit.demo1的包,那么会存在一个对应的路径com/bit/demo1来存储代码 4.如果一个类没有package语句,则将该类放到一个默认包中。
步骤如下: 1.在IDEA中先新建一个包,右键src->new>package 2.在弹出的对话框中输入包名,比如com.bit.demo1 3.在包中创建类,右键包名->新建->类,然后输入类名即可 4.可以看到我们的磁盘上的目录结构已经被IDEA自动创建了 5.同时我们也可以看到,在新建的test.java文件上方,出现了package语句
1.4包的访问权限控制
我们已经了解了类中的public和private。private里的成员只能被类的内部使用。如果,某个成员不含public和private关键字,那么这个成员可以在包内部的其他雷使用,但是不能在包外部的类使用 示例如下: 我们在一个包com.bit.demo1中创建了2个类:Demo1和Demo2 在Demo1中不加任何访问修饰符创建int val=1; 那么相同包下面的Demo2也可以用这个val 打印结果为1: 但是出了这个包,我们再来测试这段相同的代码 这里就会报错。
1.5常见的系统包
1.java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入 2.java.lang.reflect:java反射编程包 3.java.net:进行网络编程开发包 4.java.sql:进行数据库开发的支持包 5.java.util:java提供的工具程序包,集合类等,非常重要 6.java.io:I/O编程开发包
二、继承
2.1背景
代码中创建的类,主要是为了抽象现实中的一些事物(包括属性和方法) 有时候客观事物之间就存在一些关联关系,那么在表示成类和对象的时候,也会存在一定的关联。 代码示例如下:
class Dog{
public String name;
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
}
class Bird{
public String name;
public int age;
public String wing;
public void eat(){
System.out.println(name+"正在吃东西");
}
public void fly(){
System.out.println(name+"正在飞行");
}
}
我们现在写了两个类:Dog和Bird,它们作为动物有一些相同的属性和方法,比如name,age,eat(),那么我们可以把这些共性都抽象出来,放在一个Animal类里面 ,然后我们让Dog和Bird分别继承Animal类,即可达到代码重用的效果。
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
}
Animal这样被继承的类,我们称为父类、基类、或超类,对于像Dog和Bird这样的类,我们称为子类、派生类,也很好理解嘛,你父亲买的电脑,你也可以用它来打电动嘛哈哈哈。
2.2语法规则
class 子类 extends 父类{
}
1.使用extends指定父类 2.java中一个子类只能继承一个父类(C++或其他一些语言支持多继承) 3.子类会继承父类的所有public的字段和方法 4.对于父类的private的字段和方法,子类中是无法访问的 5.子类的实例中,也包含着父类的实例,可以用super关键字得到父类实例的引用 举例说明:(我们仍用上面的Dog和Bird举例)
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
}
class Dog extends Animal{
}
class Bird extends Animal{
public String wing;
public void fly(){
System.out.println("正在飞行");
}
}
public class Demo1{
public static void main(String[] args) {
Dog d=new Dog();
d.name="xiaoHei";
System.out.println(d.name);
d.eat();
}
}
运行效果如下: 我们用子类创建一个对象后,它的在内存里是这样的:
我们子类继承父类后,我们new一个子类的对象,会在堆上创建父类有的东西和子类有的东西。比如我们上面的代码,狗是动物的子类,狗这个类里面本没有东西,但继承了动物里的name,age,那么堆里面就会开辟一块内存放name和age。Bird这个类里面有wing,再继承动物这个类里面的name和age,那么堆里面就会开辟一块空间放name、age、wing
当我们在父类中加入构造方法,原先的代码就会报错了,为什么呢?
子类构造的同时,需要先帮助父类进行构造,也就是子类里面要调用父类的构造方法,这里我是这样理解的:子类要继承一些东西,你父类得有那些东西,就像是你想继承你爸爸的餐厅啥的,你爸爸得有餐厅。而优先帮父类构造也就是我们下面要说的super
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
public Animal(String name,int age){
this.name=name;
this.age=age;
}
}
class Dog extends Animal{
public Dog(String name,int age){
super(name,age);
}
}
class Bird extends Animal{
public Bird(String name,int age,String wing){
super(name,age);
this.wing=wing;
}
public String wing;
public void fly(){
System.out.println("正在飞行");
}
}
我们在Dog和Bird里面都弄一个构造方法来调用父类的构造方法, super(实际参数列表…)表示显示调用父类中的构造方法,它用来表示当前对象的父类型特征,就比如这里Dog这个类里面的super(name,age)就是表示狗子的名字和年龄,名字和年龄是父类动物的特征。
这个时候可能会有同学会问,那我们之前没有构造方法的时候为什么没有报错?在笔者上一篇java类和对象文章中,我们说过,一个类没有任何构造方法时,系统会自动生成一个无参的构造方法 那么我们这里子类父类会这样无参生成构造方法:
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
public Animal(){
}
}
class Dog extends Animal{
public Dog(){
super();
}
所以在你改变父类的构造方法后,子类原先和父类对应的无参构造就被破坏了,你也必须在子类中的构造方法做出相应的改变。 ps1:super只能在当前构造方法中使用 ps2:super在构造方法中必须处于第一行的位置,也侧面说明了,子类构造时必须先帮助父类先构造(super先进去构造父类,构造完再出来构造子类) super的三种用法: 1.super();调用父类的构造方法,只能在构造方法中使用 2.super.func();调用父类的普通方法 3.super.data;调用父类的成员属性
用法1必须在构造方法中使用,2和3出现在哪里都无所谓,这些出现位置的用法和this非常相似,我们回顾一下this的用法:
1.this.data表示调用当前对象的属性 2.this.func()表示调用当前对象的方法 3.this()调用当前对象的其他构造方法——this()只能存在于构造方法中
注意!:super不能出现在静态方法中,super是代表父类对象的引用,它是依赖于对象的,static修饰的静态方法是不依赖对象的,所以是不能共存的。this也一样,this是当前对象的引用,它也是依赖对象的,所以也不能放在静态方法中
重名:
class Animal{
public String name="abc";
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
public Animal(String name,int age){
this.name=name;
this.age=age;
}
}
class Dog extends Animal{
public String name;
public Dog(String name,int age){
super(name,age);
}
}
public class Demo1 {
public static void main(String[] args) {
Dog d=new Dog("xiaoHei",1);
System.out.println(d.name);
}
}
我们现在new了一个Dog类的d对象,传参过去xiaoHei和1(name参数和age参数),然后进入Dog类里面的构造方法,构造方法里的super把xiaoHei和1再传给父类,将父类里的name和age初始化了,现在问题是,我子类里面也有一个name,那我程序也没有报错啊,最后运行结果的name是谁呢?
我们用子类创建一个对象后,它的在内存里是这样的: 那同样的,我们现在如果Dog这个子类里面也有name这个属性,堆里面应该是有两个name,如果是子类应该是null,如果是父类应该是xiaoHei。
我们的运行结果是null,这里也就是涉及一个知识点:如果子类和父类有同名的字段了,那么是优先子类的字段。如果子类中没有父类的字段,用父类的
说个大白话:你爸爸买了一块蛋糕,你也买了一块蛋糕,你肯定优先吃你自己的蛋糕;你爸爸买了一块蛋糕,你没买蛋糕,你可以吃你爸爸的蛋糕。
那这个时候肯定也有一些小伙伴要问,那我如果子类和父类里面有同名字段,但我就是想用父类里面的东西,怎么办? 也有办法!就是我们之前说的super.name,不然默认还是优先子类
2.3protected关键字
我们在Animal里面多加一个count属性,并用private修饰
class Animal{
public String name;
public int age;
private int count;
public void eat(){
System.out.println(name+"正在吃东西");
}
public Animal(String name,int age){
this.name=name;
this.age=age;
}
}
class Dog extends Animal{
public String name;
public Dog(String name,int age){
super(name,age);
}
}
public class Demo1 {
int val=1;
public static void main(String[] args) {
Dog d=new Dog("xiaoHei",1);
System.out.println(d.name);
System.out.println(d.count);
}
}
会发现,我们在调用count这个对象的时候,会报错,是因为private只能在当前类使用,出了当前类就不能直接调用private修饰的东西,那我们把private改成protect试一试:
class Dog extends Animal{
public String name;
public Dog(String name,int age){
super(name,age);
}
}
public class Demo1 {
int val=1;
public static void main(String[] args) {
Dog d=new Dog("xiaoHei",1);
System.out.println(d.name);
System.out.println(d.count);
}
}
你会发现系统不再报错,为什么?我们先来看一张图 (图片来自比特就业课) public修饰的:什么地方都能用 private修饰的:只有当前类可以用 default修饰的:同一包中都可以用,出了这个包就不能用了 protected修饰的:同一包都可以用,或者出了包,但是是子类也可以用。
我们如果用public或者private修饰一个类,有时就过于激进了,protected好就好在,它对于子类继承有一个判断——继承了可以访问,不继承不能访问
2.4更复杂的继承关系
刚才我们的例子中,只涉及到了Animal、Dog、Bird三种类,那如果情况更复杂呢?我们Animal下面还有猫,猫下面还有更多品种的猫: (图片来自比特就业课) 这时候使用继承方式来表示,会涉及到更复杂的体系。
2.5final关键字
如果一个类不想被继承,我们可以设置为final,很好理解嘛:你不想生孩子继承你的花呗,那你就是你家族最后的final的一个。(哈哈哈开个玩笑) ps:final的一些其他用法 final int a=10;//常量不可被修改 final class A //代表整个类不可以被继承 final修饰方法,后面再讲解
三、组合
和继承类似,组合也是一种表达类之间关系的方式,也能够达到代码重用的效果。组合是a part of…的意思,比如一个学校由学生和老师组成,那么学生是学校的一部分,老师也是学校的一部分。 (图片来自比特就业课) 我们组合不需要像继承那样写extends这样的关键字,仅仅是将一个类的实例作为另一类的字段(如上图)。这是我们设计类的一种常用方法之一。
四、多态
4.1向上转型(子类对象给父类)
刚才的例子中,我们可能会写形如下面的代码:
Bird bird=new Bird("肥波");
这个代码也可以写成如下形式
Bird bird=new Bird("肥波");
Animal bird2=bird;
Animal bird2=new Bird("肥波");
此时bird2是一个父类(Animal)的引用,指向一个子类Bird的实例,这种写法称为向上转型
什么时候会发生向上转型呢? 1.直接赋值:
Animal bird2=new Bird("肥波");
2.方法传参
public static void func(Animal animal){
}
public static void main(String[] args) {
Dog dog=new Dog("xiaoHei",1);
func(dog);
}
3.作为返回值
public static Animal func2(){
Dog dog=new Dog("xiaoH",1);
return dog;
}
你在func2这个方法里new了一个dog对象,然后返回的时候把这个对象交给Animal这个类的引用来指向(你返回的时候肯定有东西接收嘛,接收那头肯定是Animal 引用名=返回值,还是相当于父类引用 引用了 子类对象)
这种向上转型我们可以这样理解: 你让你女朋友去喂肥波,你可以说:“媳妇你喂小鸟了吗”或者“媳妇你喂肥波了吗”
4.2动态绑定
我们先来看一段代码:
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
public Animal(String name,int age){
this.name=name;
this.age=age;
}
}
class Dog extends Animal{
public Dog(String name,int age){
super(name,age);
}
}
public class Demo1 {
public static void main(String[] args) {
Dog dog=new Dog("xiaoHei",1);
dog.eat();
}
}
这里我们new了一个狗子,然后传参过去Dog类,由super传到Animal类。然后我们由dog.eat()调用eat方法,打印了xiaoHei正在吃东西。
但有时候,我们的子类和父类会出现同名的方法,那我们再去调用会发生甚么事呢?我们在原先代码上稍作修改:我们给狗子这个类多写一个eat方法区别于其他动物的eat。
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(name+"正在吃东西");
}
public Animal(String name,int age){
this.name=name;
this.age=age;
}
}
class Dog extends Animal{
public String name;
public Dog(String name,int age){
super(name,age);
}
public void eat(){
System.out.println(name+"正在吃骨头");
}
}
public class Demo1 {
public static void main(String[] args) {
Dog dog=new Dog("xiaoHei",1);
dog.eat();
}
}
我们new了一个dog,然后传参过去Dog的类,再由super传参给Animal,如果我们Dog类里面没有eat方法,我们肯定会打印,xiaoHei正在吃东西,但是当子类里有和父类同名的eat会怎样呢?运行结果如下: 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.(只有父类的方法用父类的,子类和父类都有同名的用子类的)
动态绑定: 1.父类引用,引用 子类的对象 2.通过这个父类引用 调用父类 和子类同名的覆盖方法。
ps:同名的覆盖也就是下面要讲的重写(方法返回值相同,方法名相同,参数列表相同,内容不一定相同)
4.3方法重写
针对刚才的eat()方法来说:子类实现父类的同名方法,并且参数的类型和个数完全相同,这种情况称为覆盖、重写、覆写 重写要满足4个条件: 1.方法名相同 2.参数列表相同 3.返回值相同 4.父子类的情况下 一般重写的方法前我们加一个@Override
class Dog extends Animal{
public String name;
public Dog(String name,int age){
super(name,age);
}
@Override
public void eat(){
System.out.println(name+"正在吃骨头");
}
}
@Override就是告诉别人,这个方法是重写的 重写的一些注意事项: 1.静态方法不能重写 2.子类的访问修饰限定符范围应大于等于父类访问修饰限定符 (比如子类是public,父类是protected) 3.private方法不允许重写 4.final修饰的方法也不允许重写
ps:重写和重载的区别: (图片来自比特就业课) 重载的这种方式有时也称静态绑定,系统会根据你方法的参数来确定你要走哪个方法(重载的方法参数不一定相同),区别重写的动态绑定(函数名、参数、返回值必须相同)。
4.4理解多态
知道向上转型、动态绑定、方法重写后,我们就可以使用多态的形式来设计程序了。比如:我们可以写一些只关注父类的代码,就可以同时兼容各种子类的情况。 代码示例如下:
class shape{
public void draw(){
System.out.println("正在画画");
}
}
class Cycle extends shape{
@Override
public void draw(){
System.out.println("正在画圆");
}
}
class Flower extends shape{
@Override
public void draw(){
System.out.println("正在画花");
}
}
public class TestDemo {
public static void main(String[] args) {
shape s=new shape();
s.draw();
shape s1=new Flower();
s1.draw();
shape s2=new Cycle();
s2.draw();
}
}
运行结果如下: 到这里,我们就更深层次的理解多态了——通过一个引用调用同一个方法,会有不同的表现形式(取决于它引用哪个对象) ps:1.多态的大前提——一定是向上转型 使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低.
封装是让类的调用者不需要知道类的实现细节. 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可. 因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低. - 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
比如我现在要打印如下的形状 ①不用多态,如果子类比较少还好,但如果子类比较多else if就会非常麻烦
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
②用多态,这样的代码不仅简单而且高大上,容易让你的同学老师直呼:“牛逼哇塞!”
public static void drawShapes() {
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
4.5向下转型(父类对象给子类)
public static void main(String[] args) {
Animal animal=new Dog("xiaoHei",1);
Dog dog=(Dog)animal;
dog.eat();
}
但我们一般不建议这么写,就拿这个代码来说,很简单的道理:不是所有animal都是dog,这样很容易出错的!所有我们向下转型有一个大前提:就是你父类引用的对象要和你父类向下转型的对象匹配上,还是拿这块代码说:你animal接收了一个dog,那你animal后面向下转型只能给dog,不能给bird等等其他
public static void main(String[] args) {
Animal animal=new Dog("xiaoHei",1);
Bird bird=(Bird)animal;
bird.fly();
}
比如上面这块代码,我们new了一个狗赋给动物(相当于狗是动物),然后你又把动物赋给鸟,前后联系起来就相当于狗是鸟,这明显逻辑不通。代码运行时也确实会报错:类型转换异常。
为了让向下转型更加安全,我们有了instanceof,instanceof可以判断一个引用是否是某个类的实例,如果是,则返回true,这时再进行向下转型操作就比较安全了。
Animal animal=new Cat("小猫");
if(animal instanceof Bird){
Bird bird=(Bird)animal;
}
4.6super关键字
前面的代码中,由于使用了重写机制,调用到的是子类的方法,如果需要在子类内部调用父类方法怎么办?可以使用super关键字。 super表示获取到父类实例的引用,涉及两种常见用法: 1.使用super来调用父类的构造器(前面已经讲过)
public Bird(String name){
super(name);
}
2.使用super来调用父类的普通方法
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void eat(String food) {
super.eat(food);
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
}
在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法
super和this有一些相似,但其中也有区别: (图片来自比特就业课)
4.7在构造方法中调用重写的方法
我们先来看一段坑人的代码:
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
这里我们new了一个D类对象,然后进去D,因为D中默认会有一个super(),这里没有写,但是我们要知道super是存在的,然后super进去父类B构造方法,B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func,此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0。
五、抽象类
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).
5.1语法规则
abstract class Shape {
public abstract void draw();
public int a;
public void func(){
System.out.println("一个普通方法");
}
}
class Rect extends shape{
@Override
public void draw() {
System.out.println("正在画方块");
System.out.println(a);
super.func();
}
}
class Cycle extends shape{
@Override
public void draw(){
System.out.println("正在画圆");
}
}
class Flower extends shape{
@Override
public void draw(){
System.out.println("正在画花");
}
}
注意事项:
- 抽象类不能直接实例化
shape shape = new Shape();
Error:(30, 23) java: Shape是抽象的; 无法实例化
- 抽象方法不能是 private 的
这个很好理解,抽象类要被其他子类继承的啊,你老爹把东西都藏起来了,你继承个啥啊
abstract class Shape {
abstract private void draw();
}
Error:(4, 27) java: 非法的修饰符组合: abstract和private
- 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用
abstract class Shape {
public abstract void draw();
public int a;
public void func(){
System.out.println("一个普通方法");
}
}
class Rect extends shape{
@Override
public void draw() {
System.out.println("正在画方块");
System.out.println(a);
super.func();
}
}
一些特殊的注意点:
1.如果一个抽象类继承了另一个抽象类,那这个抽象类可以不重写父类的抽象方法
abstract class shape{
public int a;
public void func(){
System.out.println("一个普通方法");
}
public abstract void draw();
}
abstract class A extends shape{
public abstract void func2();
}
2.但如果继承抽象类的抽象类再次被继承(比如上面的类A被继承了),你就必须把之前欠的抽象方法全部写上
abstract class shape{
public int a;
public void func(){
System.out.println("一个普通方法");
}
public abstract void draw();
}
abstract class A extends shape{
public abstract void func2();
}
class B extends A{
@Override
public void draw() {
}
@Override
public void func2() {
}
}
大家可以按我这个理解方式来进行简单的记忆:抽象方法相当于你开的花呗,你爸爸不还,你来还,你仍然不还,你儿子来还。。。出来混迟早要还的哈哈哈
5.2抽象类的作用
抽象类存在的最大意义就是为了被继承. 抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法 这个时候可能会有人说:普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?确实如此. 但是使用抽象类相当于多了一重编译器的校验
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题. 小结:
六、接口
接口是抽象类的更进一步(你可以理解它是一个特殊的抽象类). 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量.
6.1语法规则
代码如下:
interface IShape{
public abstract void draw();
default public void func(){
System.out.println("第一个默认方法");
}
default public void func2(){
System.out.println("第二个默认方法");
}
public static void staticFunc(){
System.out.println("静态方法");
}
public static final int a=1;
}
class Triangle implements IShape{
@Override
public void draw() {
System.out.println("画一个三角");
}
@Override
public void func(){
System.out.println("重写接口中默认方法");
}
}
public class TestDemo {
public static void main(String[] args) {
IShape iShape=new Triangle();
iShape.draw();
iShape.func();
}
}
运行结果如下: 一些注意点: 1.使用interface定义一个接口
2.接口中的方法一定是抽象方法,因此可以省略abstract
3.接口中的方法一定是public,因此可以省略public
4.我们代码中的Triangle使用implements继承接口,此时表达的含义不再是“扩展”而是“实现”
5.在调用的时候同样可以创建一个接口的引用,对应到一个子类的实例(向上转型,比如上述代码中的 IShape iShape=new Triangle();)
6.接口不能单独被实例化(你不能new一个接口) 扩展(extends):当前已有一些功能,进一步扩充功能 实现(implements):当前什么也没有,需从头构造
6.2实现多个接口
如下面代码中的c,如果想同时实现Ia和Ib两个接口,可以通过implements来实现,多个接口由逗号隔开。比如class c implements Ia,Ib 这里需要注意的是,实现多个接口,那你也必须需要重写每一个接口的抽象方法
interface Ia{
void funcA();
}
interface Ib{
void funcB();
}
class c implements Ia,Ib{
@Override
public void funcA() {
}
@Override
public void funcB() {
}
}
这里需要注意的是,抽象方法的顺序必须对应多个接口的顺序,比如Ia的必须对应funcA,Ib的必须对应funcB。
举例说明: 有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的. 然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.现在我们通过类来表示一组动物
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”.
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
接下来我们创建几个具体的动物 猫, 是会跑的.
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
鱼, 是会游的.
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在用尾巴游泳");
}
}
还有一种神奇的动物, 水陆空三栖, 叫做 鸭子
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在漂在水上");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口. 继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 . 猫是一种动物, 具有会跑的特性. 青蛙也是一种动物, 既能跑, 也能游泳 鸭子也是一种动物, 既能跑, 也能游, 还能飞
几组测试案例:
public class test {
public static void running(IRunning iRunning) {
iRunning.run();
}
public static void swimming(ISwimming iSwimming) {
iSwimming.swim();
}
public static void flying(IFlying iFlying){
iFlying.fly();
}
public static void main(String[] args) {
running(new Duck("鸭子"));
flying(new Duck("鸭子"));
swimming(new Fish("鱼"));
}
}
运行结果如下:
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.
6.3接口的继承
接口和接口之间,可以使用extends来操作它们的关系(意为拓展),一个接口IB通过extends来拓展另一个接口IA的功能。当一个类Ic通过implements来实现接口IB时,重写的方法不仅仅是IB的抽象方法,还有IB从C接口拓展来的抽象方法funcA 代码示例如下:
interface IA{
void funcA();
}
interface IB extends IA{
void funcB();
}
class Ic implements IB{
@Override
public void funcB() {
}
@Override
public void funcA() {
}
}
6.4Clonable接口和深拷贝
Java 中内置了一些很有用的接口, Clonable 就是其中之一
Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常.
class Animal implements Cloneable {
private String name;
@Override
public Animal clone() {
Animal o = null;
try {
o = (Animal)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Animal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
浅拷贝 VS 深拷贝 Cloneable 拷贝出的对象是一份 “浅拷贝” 观察以下代码:
public class Test {
static class A implements Cloneable {
public int num = 0;
@Override
public A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
static class B implements Cloneable {
public A a = new A();
@Override
public B clone() throws CloneNotSupportedException {
return (B)super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
B b = new B();
B b2 = b.clone();
b.a.num = 10;
System.out.println(b2.a.num);
}
}
通过 clone 拷贝出的 b 对象只是拷贝了 b 自身, 而没有拷贝内部包含的 a 对象. 此时 b 和 b2 中包含的 a 引用仍然是指向同一个对象. 此时修改一边, 另一边也会发生改变. 未来学到序列化的时候, 会告诉大家如何进行深拷贝.
6.6小结
抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(非常重要!! ). 核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法. 如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此,此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
|