目录
序
上一节讲了有关类与对象初步认知的一些知识——实例化、构造方法、this和static关键字、代码块、类的封装以及匿名对象。
如果还有不了解的小伙伴可以传送这篇——【Java】类与对象(上)
这一节讲的语法就抽象很多了,难度也更大,细碎的东西非常多——大致包括final关键字、类的继承、抽象类和接口以及多态的知识,为了写这篇博客整合了半天,话不多说,我们开始吧——
一、包
1、导入包中的类
1.1、使用的时候导入
在要使用的 Java 提供的类的前面加上 java.包名.类 。如下例,导入 java.util 包中的 Date 类
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
System.out.println(date.getTime());
}
}
1.2、用 import 语句导入
如下例, 在最开头使用import导入后,就可以直接用 Java提供的类了。
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date.getTime());
}
}
1.3、 用 java.util.* 导入
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
Date date = new Date();
System.out.println(date.getTime());
}
}
// 编译出错
Error:(5, 9) java: 对Date的引用不明确
java.sql 中的类 java.sql.Date 和 java.util 中的类 java.util.Date 都匹配
如上例,这种时候要特殊指定一下类,将第六行语句改成
java.util.Date date = new java.util.Date();
即可编译通过。
2、静态导入
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println("hello");
}
}
例二:
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
double x = 30;
double y = 40;
//如果没有导入则需要写成 double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
double result = sqrt(pow(x, 2) + pow(y, 2));
System.out.println(result);
}
}
3、将类放在包中
(a)在文件的最上方加上一个 package 语句指定该代码在哪个包中。如果一个类没有 package 语句,则该类被放到一个默认包中。
(b)包名需要尽量指定成唯一的名字。
(c)包名要和代码路径相匹配,例如创建 com.csdn.demo 的包,那么会存在一个对应的路径 com/csdn/demo 来存储代码。
具体操作步骤
1、在idea中新建一个包。
2、在对话框中输入包名。
3、在该包中创建类。
4、此时,我们就可以发现磁盘上的目录结构已经被 IDEA 自动创建出来了,且新建的文件中就出现了package语句。
4、包的访问控制权限
5、常见的系统包
二、继承
在现实生活中,说到继承,多会想到子女继承父辈的财产、事业等等。在程序中,继承描述的是事物之间的所属关系。
在Java中,类的继承是指在一个现有的类基础上去构建一个新的类,构建出来的新类被称作子类、派生类,现有的类被称作父类、基类或超类,子类会自动拥有父类所有可以继承的属性和方法。
1、基本语法
[修饰符] class 子类 extends 父类 {}
❤ 子类的实例中,也包含着父类的实例。可以使用 super 关键字得到父类实例的引用。
❤ 子类会继承父类的所有 public 的字段和方法。对于父类的 private 的字段和方法,子类中是无法访问的。
❤ 类的修饰符是可选的,用来指定类的访问权限。如果不写则默认为包访问权限。
❤ Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承),但是多个类可以继承同一个父类,也可以多层继承(套娃了属于是)
接下来我们看一个例子帮助我们更好理解:
class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Cat extends Animal {
public Cat(String name) {
super(name); // 使用 super 调用父类的构造方法(等会就会讲到)
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "正在飞");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat("小喵");
cat.eat("猫粮");
Bird bird = new Bird("圆圆");
bird.fly();
}
}
//运行结果
小喵正在吃猫粮
圆圆正在飞
2、方法的重写
3、 super 关键字
当子类重写父类的方法后,子类对象将无法直接访问父类被重写的方法。为了解决这个问题,在Java中专门提供了一个关键字来访问父类的成员——成员变量、成员方法和构造方法。
(1)使用 super 关键字调用父类的成员变量与成员方法
具体格式:
super.成员变量
super.成员方法(参数1,参数2....)
接下来通过个案例看看如何使用 super 关键字:
class Animal{
String name="动物";
void shout(){
System.out.println("动物发出叫声");
}
}
class Dog extends Animal{
String name="犬类";
void shout(){
super.shout();
}
void printName(){
System.out.println("name="+super.name);
}
}
public class Test{
public static void main(String[] args) {
Dog dog=new Dog();
dog.shout();
dog.printName();
}
}
//运行结果
动物发出叫声
name=动物
(2)使用 super 关键字调用父类的构造方法
具体格式:
super(参数1,参数2...)
接下来通过一个案例来学习如何使用super关键字调用父类的构造方法:
class Animal{
public Animal(String name){
System.out.println("我是一只"+name);
}
}
class Dog extends Animal{
public Dog(){
super("沙皮狗");
}
}
public class Test{
public static void main(String[] args) {
Dog dog=new Dog();
}
}
//运行结果
我是一只沙皮狗
注意事项:
1、通过 super 调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次。
2、在子类的构造方法中一定会调用父类的某个构造方法。如果没有指定,在实例化子类对象时,会默认调用父类无参的构造方法。(此时就很容易出错,如果父类只定义了有参的构造方法,而子类没有显示的调用父类的构造方法时,就会出现编译错误)
因此,为了防止这种情况的发生,我们在定义一个类时,如果没有特殊需求,当定义了有参构造方法后,尽量在类中再显式地定义一个无参的构造方法,这样可以避免该类被继承时出现错误。
到这,用法就讲的差不多了。此时,我们回过头想想,this 和 super 有哪些区别呢?
区别 | this | super |
---|---|---|
概念 | 访问本类中的属性和方法 | 由子类访问父类的属性和方法 |
查找范围 | 先查找本类,如果本类没有就调用父类 | 不查找本类而直接调用父类 |
4、protected 关键字及四种访问权限
范围 | private | default | protected | public |
---|---|---|---|---|
同一包中的同一类 | √ | √ | √ | √ |
同一包中的不同类 | √ | √ | √ | |
不同包中的子类 | √ | √ | ||
不同包中的非子类 | √ |
5、final关键字
基本用法:
1、[修饰符] final 返回类型 方法名(){ }
2、final 类型 变量名
final关键字可以用于修饰类、变量和方法,它有“不可更改”或者“最终的含义”,因此被 final 修饰的类、变量和方法有以下特性:
(1)final 修饰的类不能被继承。
(2)final 修饰的方法不能被子类重写。
(3)final 修饰的变量(成员变量和局部变量)是常量,只能赋值一次。
而修饰变量时,又分两种情况:
1、当 final 修饰局部变量时,可以在声明变量的同时对变量进行赋值,也可以先声明变量然后再进行有且只有一次的赋值。
2、当 final 修饰成员变量时,在声明变量的同时必须进行初始化赋值,否则程序编译报错。
三、抽象类
当定义一个类的时候,常需要定义一些方法来描述该类的行为,但有时候这些方法的实现方式是无法确定的,此时就可以用到抽象类来满足这种需求——能使一个类包含所需的方法,又无须提供其方法的实现。
抽象类以及抽象方法的基本语法格式如下:
[修饰符] abstract class 类名{
[修饰符] abstract 方法返回值类型 方法名 ([参数列表]);
}
注意:
1、包含抽象方法的类必须定义为抽象类,但抽象类中可以不包含任何抽象方法。
2、抽象类是不可以被实例化的(因为抽象类中有可能包含抽象方法,抽象方法没有方法体,不可以被调用);如果想调用抽象类中定义的抽象方法,需要创建一个子类,在子类中实现抽象类中的抽象方法。
3、抽象方法不能是 private 的。(无法被重写)
4、抽象类中可以包含其他的非抽象方法,也可以包含字段。这个非抽象方法和普通方法的规则都是一样的,可以被重写,也可以被子类直接调用。
5、抽象类与抽象方法都不能被 final 修饰。(抽象类就是为了被继承而存在,被 final 修饰就无法被继承,同理抽象方法也不能被重写 )
6、
- 一个普通类,继承了一个抽象类,那么这个普通类当中,需要重写这个抽象类中的所有抽象方法。
- 一个抽象类A,继承了一个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法。
- 如果当抽象类 A 再次被一个普通类继承时,这个普通类必须重写A和B两个抽象类中的所有抽象方法。(颇有种“出来混,迟早是要还的”的感觉)
四、接口
1、接口的定义
[修饰符] interface 接口名 [extends 父接口1,父接口2...]{[public] [static] [final] 常量类型 常量名 = 常量值 ;[public] [abstract] 返回值类型 方法名(参数列表);[public] default 返回值类型 方法名(参数列表){//默认方法的方法体}[public] static 返回值类型 方法名(参数列表){//静态方法的方法体}}
在上述语法格式中:
1、"[ ]"中的内容都是可选的,修饰符可以使用 public 或者直接省略(省略则默认采用包访问权限)
2、“extends 父接口1,父接口2” 表示定义一个接口时,可以同时继承多个父接口,这也是为了解决类的单继承的限制。
3、常量定义时必须进行初始化赋值,定义默认方法与静态方法时,可以有方法体。
4、接口不能单独被实例化(抽象类都不能,接口显然更不能了)
5、在接口定义常量时,可以省略 “public static final” 修饰符。与此类似,定义抽象方法时,也可以省略 “public abstract” 修饰符,定义默认方法与静态方法时,可以省略 “public” 修饰符。这些修饰符系统都会默认添加。
2、接口的实现类
基本语法:
[修饰符] class 类名 [extends 父类名 ] [implements 接口1,接口2] {
.......
}
接下来用一个实例演示一下:
interface Animal{
int ID=1;
void breathe();
default void getType(String type){
System.out.println("该动物属于:"+type);
}
static int getId(){
return Animal.ID;
}
}
class Dog implements Animal{
public void breathe(){
System.out.println("狗在呼吸");
}
}
public class Main{
public static void main(String[] args) {
System.out.println(Animal.getId());
Dog dog=new Dog();
System.out.println(dog.ID);
dog.breathe();
dog.getType("犬科");
}
}
//运行结果
1
1
狗在呼吸
该动物属于:犬科
上述例子演示的是类与接口之间的关系。其实,接口与接口之间还可以是继承关系,接口中的继承同样使用 extends 关键字来实现。接下来对上述例子进行修改演示接口之间的继承关系。
interface Animal{
int ID=1;
void breathe();
default void getType(String type){
System.out.println("该动物属于:"+type);
}
static int getId(){
return Animal.ID;
}
}
interface LandAnimal extends Animal{
void run();
}
class Dog implements LandAnimal{
public void breathe(){
System.out.println("狗在呼吸");
}
public void run(){
System.out.println("狗在陆地上跑");
}
}
public class Main{
public static void main(String[] args) {
System.out.println(Animal.getId());
Dog dog=new Dog();
System.out.println(dog.ID);
dog.breathe();
dog.getType("犬科");
dog.run();
}
}
//运行结果
1
1
狗在呼吸
该动物属于:犬科
狗在陆地上跑
注意:
1、接口名一般用 I+英文名 ,比较好区分。
2、接口中有三类方法:静态方法、抽象方法和默认方法。静态方法可以通过“接口名.方法名”的形式来调用;而抽象方法和默认方法只能通过接口实现类的实例对象来调用。
3、
- 当一个类实现接口时,如果这个类是抽象类,只需实现接口中的部分抽象方法即可。如果一个非抽象类实现接口时,则需要实现接口中的所有抽象方法。
- 如果一个接口A继承了另一个接口B,当一个普通类实现接口A的时候,则需要实现两个接口中包含的所有抽象方法。(与之前讲到的的类的继承有异曲同工之妙~)
4、一个类可以通过 implement 关键字同时实现多个接口。接口之间可以通过 extends 关键字实现继承,并且一个接口可以同时继承多个接口。
5、当一个类实现一个接口后,重写的这个方法前面必须加上 public 。(思考一下这是为什么呢?答案很简单,但是需要串联前面的知识——因为子类重写的方法的限制不能比父类更严格,而父类,也就是接口,包含的类都是 public 的,所以子类重写的方法前当然得加上 public )
6、一个类在继承一个类的同时还可以实现接口,此时,extends 关键字必须位于 implement 关键字之前。
class A extends B implements C{
.....//先继承,再实现
}
五、多态
1、多态的概述
在Java中,多态是指不同类的对象在调用同一个方法时所呈现出的多种不同行为。通常来说,在一个类中定义的属性和方法被其他类继承或重写后,当把子类对象直接赋值给父类引用变量时,相同引用类型的变量调用同一个方法将呈现多种不同形态。
Java的多态性是由类的继承、方法重写以及父类引用指向子类对象体现的。由于一个父类可以有多个子类对象,多个子类都可以重写父类的方法,并且多个不同子类对象也可以指向同一个父类;这样程序运行时才能知道具体代表的是哪个子类对象。这就体现了多态性。
2、向上转型
我们先看下面一段代码:
abstract class Animal{
abstract void shout();
}
class Cat extends Animal{
public void shout(){
System.out.println("喵喵!");
}
}
class Dog extends Animal{
public void shout(){
System.out.println("汪汪!");
}
}
public class Main{
public static void main(String[] args) {
Animal an1=new Cat();
Animal an2=new Dog();
an1.shout();
an2.shout();
}
}
//运行结果
喵喵!
汪汪!
由上述程序,我们可以看出程序在编译时自动识别具体的子类对象,从而选择性的调用对应的方法,这就是Java中多态性的体现。
上述程序还涉及了一个很重要的点——将子类对象当作父类类型使用,而这种情况在Java中被称为“向上转型”。如下:
Animal an1=new Cat( );//将Cat类对象当作Animal类型来使用
Animal an2=new Dog();//将Dog类对象当作Animal类型来使用
为啥叫“向上转型”呢?
4、向下转型
看一段代码:
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
public void fly() {
System.out.println(this.name + "正在飞");
}
}
public class Main{
public static void main(String[] args) {
Animal animal = new Bird("哈哈");
animal.eat("谷子");
// (Bird) 表示强制类型转换
Bird bird = (Bird)animal;
bird.fly();
}
}
//运行结果
我是一只小鸟
哈哈正在吃谷子
哈哈正在飞
分析 “Animal animal = new Bird("哈哈"); ” 这行代码,我们可以得出:
1、编译器检查有哪些方法存在, 看的是 Animal 这个类型。
2、执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型。
3、将子类对象当作父类使用时,不需要任何显式声明,但是此时不能通过父类变量去调用子类特有的方法。如果非要调用,则需“向下转型”。
但是有时候,这个向下转型又不大靠谱,比如:
Animal animal = new Cat(" 小猫 ");Bird bird = (Bird)animal;bird.fly();// animal 本质上引用的是一个 Cat 对象 , 是不能转成 Bird 对象的。
instanceof 关键字
语法格式如下:
对象(或对象引用变量)instanceof 类(或接口)
Animal animal = new Cat("小猫");
if(animal instanceof Bird){
Bird bird = (Bird)animal;
bird.fly();
}else{
System.out.println("该类型的对象不是 Bird 类型!")
}
这时再进行类型转换就安全多了。
5、动态绑定
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Animal("哈哈");
animal1.eat("谷子");
Animal animal2 = new Bird("嘿嘿");
animal2.eat("谷子");
}
}
//运行结果
我是一只小动物
哈哈正在吃谷子
我是一只小鸟
嘿嘿正在吃谷子
由上述程序运行结果来看,我们发现:
1、animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向 Bird 类型的实例。
6、内部类
在一个类的内部定义类,这样的类称作内部类。这个内部类所在的类称为外部类。在实际情况中,内部类分为四种形式:成员内部类、局部内部类、静态内部类、和 匿名内部类。
6.1、成员内部类
在一个类中定义的类,被称为成员内部类。在成员内部类中,可以访问外部类的所有成员,包括成员变量和成员方法;在外部类中,同样可以访问成员内部类的变量和方法。
例:
class Outer{
int m=0;
void test1(){
System.out.println("外部类成员方法!");
}
class Inner{
int n=1;
void show1(){
System.out.println("外部成员变量 m="+m);
test1();
}
void show2(){
System.out.println("内部成员方法!");
}
}
void test2(){
Inner inner=new Inner();
System.out.println("内部成员变量 n="+inner.n);
inner.show2();
}
}
public class Main{
public static void main(String[] args) {
Outer outer=new Outer();
Outer.Inner inner=outer.new Inner();
inner.show1();
outer.test2();
}
}
//运行结果
外部成员变量 m=0
外部类成员方法!
内部成员变量 n=1
内部成员方法!
上述要注意的是,“ Outer.Inner inner=outer.new Inner( ); ” 是通过外部类对象创建的内部类对象,这样就可以操作内部类中的成员。
创建内部类对象的具体格式如下:
外部类名.内部类名 变量名 = new 外部类名( ).new 内部类名( );
或 外部类名.内部类名 变量名 = 外部对象名.new 内部类名( );
6.2、局部内部类
局部内部类,又名方法内部类,就是定义在某个局部范围中的类,它和局部变量一样,都是在方法中定义的,其有效范围只限于方法内部。
在局部内部类中,局部内部类可以访问外部类的所有成员变量和方法,而局部内部类中的变量和方法却只能在创建该局部内部类的方法中进行访问。
接下来看个例子如何运用:
class Outer{
int m=0;
void test1(){
System.out.println("外部类成员方法!");
}
void test2(){
class Inner{
int n=1;
void show(){
System.out.println("外部类变量 m="+m);
test1();
}
}
Inner inner=new Inner();
System.out.println("局部内部变量 n="+inner.n);
inner.show();
}
}
public class Main{
public static void main(String[] args) {
Outer outer=new Outer();
outer.test2();
}
}
//运行结果
局部内部变量 n=1
外部类变量 m=0
外部类成员方法!
上述例子再一次验证了:
局部内部类可以访问外部类的所有成员,而只有在包含局部内部类的方法中才可以访问局部内部类的所有成员。
6.3、静态内部类
静态内部类,就是用 static 关键字修饰的成员内部类。在功能上,与成员内部类相比,静态内部类中只能访问外部类的静态成员。如果通过外部类访问静态内部类成员时,可以跳过外部类而直接通过内部类访问静态内部类成员。
1、创建静态内部类对象的基本语法格式为:
在非外部类中:外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名 ( );
在外部类中:内部类名 变量名 = new 内部类名 ( ) ;
2、外部类调用静态内部类中的属性和方法:
(1)通过创建静态内部类实例来调用其非静态属性和方法。
(2)通过 内部类.属性 / 内部类.方法 的方式直接调用其静态属性和方法。
看个例子帮助我们更好理解其用法(最好上手写一遍会更快掌握):
class Outer{
static int m=0;
static class Inner{
int n=2;
static int i=5;
void show(){
System.out.println("外部类变量 m="+m);//在静态内部类中访问外部类的成员变量
}
static void show2(){
System.out.println("调用了inner的静态方法!");
}
}
public void test(){
Inner inner=new Inner();
System.out.println(inner.n);//访问静态内部类的非静态变量
inner.show();//访问静态内部类的非静态方法
}
public void display(){
System.out.println(Inner.i);//调用静态内部类的静态成员变量
Inner.show2();//调用静态内部类的静态方法
}
}
public class Main{
public static void main(String[] args) {
Outer.Inner inner=new Outer.Inner();
inner.show(); //在非外部类中调用静态内部类的非静态方法
Outer outer=new Outer();
outer.test();
outer.display();
}
}
//运行结果
外部类变量 m=0
2
外部类变量 m=0
5
调用了inner的静态方法!
6.4、匿名内部类
在Java中调用某个方法时,如果该方法的参数是一个接口类型,除了可以传入一个参数接口实现类,还可以使用匿名内部类实现接口来作为该方法的参数。匿名内部类也就是没有名字的内部类。正因为没有名字,所以匿名内部类只能使用一次(跟匿名对象有相似之处),但使用匿名内部类还有个前提:必须继承一个父类或实现一个接口。
abstract class Animal {
public abstract void test();
}
public class Main {
public static void main(String[] args) {
//第一种使用匿名内部类的方法
Animal rabbit= new Animal() {
public void test() {
System.out.println("第一种调用方法");
}
};
rabbit.test();
//第二种使用匿名内部类的方法
new Animal(){
public void test(){
System.out.println("第二种调用方法");
}
}.test();
}
}
//运行结果
第一种调用方法
第二种调用方法
在上述例子中,相信大家应该对匿名内部类有了初步的了解,但是初学者不要求完全掌握这种写法,只需理解这些语法即可。
===================
最后再回顾、总结一下抽象类和接口——
区别 | 抽象类 | 接口 |
---|---|---|
组成 | 普通类 + 抽象方法 | 抽象方法 + 静态、默认方法(可带方法体)+ 全局常量 |
权限 | public、protected、default(默认为包访问权限) | 全为public |
子类使用 | 使用extends关键字继承抽象类 | 使用 implements关键字实现接口 |
子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
联系 | 一个抽象类可以实现多个接口 | 接口不能继承抽象类,但是能用extends继承多个父接口 |
==============================
这节就到这里啦,我们下节再见!
to be continue →
欢迎大家互相探讨交流补充 欢迎捉虫!
欢迎在学习Java的小伙伴互关,一起成长进步~