(材料源于网络)
Java SE 4th day:Object-oriented 03
1、本次课程知识点
1、static关键字的作用;
2、代码块的作用;
3、内部类的使用,并且使用内部类再次修改链表操作
2、具体内容
2.1 static关键字(重点)
在之前定义方法的时候都是使用了:public static开头;
后来到了类之中后定义方法使用:public开头,不再使用static了。
在java之中使用static可以完成两个操作:一个是定义属性,另外一个是定义方法。
2.1.1 static定义属性
每一个对象都要保存自己的属性信息,即:每个对象的属性都是自己所拥有的,但是如果现在假设有如下一道程序。
范例:定义一个表示北京人的操作类,所以人的城市都属于北京。
class Person { private String name; private int age; String city = "北京";
// 按照简单java类的要求,应该提供无参,但是本程序为了简便不再写与本程序无关的代码 public Person(String name, int age) { this.name = name; this.age = age; }
public String getPersonInfo() { return "姓名: " + this.name + ",年龄: " + this.age + ",城市: " + this.city; } }
public class Demo { public static void main(String args[]) { Person per1 = new Person("张三", 20); Person per2 = new Person("李四", 25); Person per3 = new Person("王五", 30); System.out.println(per1.getPersonInfo()); System.out.println(per2.getPersonInfo()); System.out.println(per3.getPersonInfo()); } } |
姓名: 张三,年龄: 20,城市: 北京 姓名: 李四,年龄: 25,城市: 北京 姓名: 王五,年龄: 30,城市: 北京 |
如果说现在的city信息要进行修改,由于每个实例化对象都要各自占着自己的内存空间,所以肯定要修改所以的city属性信息,但是如果说现在已经产生了1000w个Person对象,那么这个工作量肯定很大,而且所以的Person对象的city属性既然都一样,这样分开保存也浪费空间,那么这个时候就希望可以将city定义成一个公共的属性,每一个对象都拥有,而且可以单独的在一块空间内保存的,完成此功能就需要static关键字的支持了。
class Person { private String name; private int age; staticString city = "北京";
// 按照简单java类的要求,应该提供无参,但是本程序为了简便不再写与本程序无关的代码 public Person(String name, int age) { this.name = name; this.age = age; }
public String getPersonInfo() { return "姓名: " + this.name + ",年龄: " + this.age + ",城市: " + this.city; } }
public class Demo { public static void main(String args[]) { Person per1 = new Person("张三", 20); Person per2 = new Person("李四", 25); Person per3 = new Person("王五", 30); System.out.println(per1.getPersonInfo()); System.out.println(per2.getPersonInfo()); System.out.println(per3.getPersonInfo()); System.out.println("=============================="); per1.city = "北平"; // 只修改了一个对象的city属性 System.out.println(per1.getPersonInfo()); System.out.println(per2.getPersonInfo()); System.out.println(per3.getPersonInfo()); } } |
姓名: 张三,年龄: 20,城市: 北京 姓名: 李四,年龄: 25,城市: 北京 姓名: 王五,年龄: 30,城市: 北京 ============================== 姓名: 张三,年龄: 20,城市: 北平 姓名: 李四,年龄: 25,城市: 北平 姓名: 王五,年龄: 30,城市: 北平 |
一个对象修改了city属性之后,发现其他的都变化了,所以此时的内存关系图如下:
通过程序和内存关系图可以发现,使用static定义的属性将成为公共属性,每一个对象都拥有它,一个对象修改了内容,其他所有的对象都要修改,可是这样的代码也不合适,既然static定义的公共属性,那么公共属性由一个对象直接修改合适吗?很明显不合适,类的公共属性应该由类进行修改是最合适的,因为用户不知道一个类到底有多少个对象产生。
使用static定义的属性有时候又被成为类属性,而且类属性的最大特点是可以直接通过类名称调用,而且调用的时候可以没有实例化产生的情况下进行。
类属性调用格式:
类名称.static属性 |
范例:使用类名称直接调用类属性
…… static String city = "北京"; …… public static void main(String args[]) { Person.city = "北平"; // 现在还没有实例化对象产生 System.out.println(Person.city); } |
北平 |
下面可以给出一些常见的内存分配:
●栈内存:保存的是一块堆内存的访问地址;
● 堆内存:保存的是类中普通属性;
● 全局数据区:保存所有的全局数据,例如:static就是全局数据;
● 全局代码区:保存所有的操作方法;
虽然static属性在类中定义,但是独立于类之外的产物。
2.1.2 static定义方法
使用static除了可以进行类属性的定义之外,也可以进行类方法的定义,而且定义的类方法可以由类名称直接调用。
class Person { private String name; private int age; static String city = "北京";
public Person(String name, int age) { this.name = name; this.age = age; }
public static void setCity(String c) { city = c; }
public String getPersonInfo() { return "姓名: " + this.name + ",年龄: " + this.age + ",城市: " + this.city; } }
public class Demo { public static void main(String args[]) { Person.setCity("北平"); // 现在还没有实例化对象产生 System.out.println(Person.city); } } |
北平 |
与static属性一样,对于static方法实际上也应该是独立于类之外的产物,也可以在没有实例化对象的情况下进行调用,而且现在对于类中的方法就分为了两种:
● 非static方法:必须由实例化对象进行调用,而且使用非static方法可以调用static方法或属性;
● static方法:可以由类名称调用,或者由实例化对象调用,而且所有的static方法不能调用非static属性或方法;
分析:为何会存在此类限制呢?
很明显对于所有的普通属性和普通方法,前提是必须有一个实例化对象产生,这样才能为属性分配堆内存空间,而当一个实例化对象产生之后,很明显可以直接调用static属性或方法,因为这些方法可以在没有实例化对象的时候就被调用。
使用static定义的方法,由于可以在没有实例化对象产生的情况下被调用,所以操作中如果调用了普通属性,而又没有实例化对象产生的时候,普通属性根本就没有堆内存开辟,所以无法调用。
而学习完了此概念之后,就可以解决下面的一个问题了,在最早的时候曾经强调过,如果现在一个方法由主方法直接调用的话,则方法的定义格式如下:
public static 返回值类型 方法名称(参数列表){ [return [返回值] ] } |
后来学习类之后,方法的定义格式又变为:
public 返回值类型 方法名称(参数列表){ [return [返回值] ] } |
范例:主方法直接调用
public class Demo { public static void main(String args[]) { print(); }
public staticvoid print() { System.out.println("Hello World. "); } } |
Hello World. |
之所以由主方法直接调用的方法前必须使用static,主要的原因也是因为主方法使用了static,即:static方法可以直接调用static操作,而不能调用非static操作。
范例:调用非static操作
public class Demo { public static void main(String args[]) { new Demo().print(); }
public void print() { System.out.println("Hello World. "); } } |
Hello World. |
对于普通方法肯定要由实例化对象进行调用,可是从开发而言,在主类之中,一般不建议定义其他方法,主类中最好就只有一个主方法。
2.1.3 主方法
下面解释一下主方法的组成:public static void main(String args[]):
● public:表示的是一种操作权限,指的是此方法被所有人所看见;
● static:表示方法可以有类名称直接调用,执行类:java 类名称;
● void:主方法是作为程序的起点存在,一旦开始了就没有回头路;
● main:是一个系统规定好的方法名称,执行类的时候默认调用此方法;
● String args[]:表示传递的接收参数;
范例:接收参数,接收的所有参数的类型都是String型的数据
public class Demo { public static void main(String args[]) { for (int i = 0; i < args.length; i++) { System.out.println(args[i]); } } } |
而参数是在运行类的时候输入的,每一个参数之间使用“”区分,例如:java Demo first second
如果参数本身包含空格,则需要使用“"”把参数引起来,例如:java Demo "hello world" "hello mldn"。
对于参数,大部分情况下不会使用的,可是主方法的组成一定要记住,包括其每一个关键字的作用,对于public是一种访问权限,以后会有更多访问权限的介绍:public > private。
2.1.4 static应用
实际上对于开发而言,static的应用主要有两点:
● 使用static属性主要用于表示公共变量的保存;
● 使用static方法一般都是在用于取得类的实例化对象的操作上,或者是一些不希望有对象的类中使用。
1、 static属性的应用
Static属性的最大特点就在于其是公共属性,那么开发之中可以利用这个属性进行实例化对象个数的统计,例如,每一个新的实例化对象都要执行构造方法,所以可以在构造方法中进行统计。
class Person { private static int count = 0; // 统计个数
public Person() { System.out.println("实例化对象的个数:" + ++count); } }
public class Demo { public static void main(String args[]) { new Person(); new Person(); new Person(); } } |
实例化对象的个数:1 实例化对象的个数:2 实例化对象的个数:3 |
对于对象的回收,暂时没有办法统计。
范例:现在也可以利用static进行属性的自动命名
class Person { private static int count = 0; // 统计个数 private String name;
public Person() { this("无名氏 - " + count++); // 自动命名 }
public Person(String name) { this.name = name; }
public String getName() { return this.name; } }
public class Demo { public static void main(String args[]) { System.out.println(new Person("张三").getName()); System.out.println(new Person().getName()); System.out.println(new Person().getName()); System.out.println(new Person("李四").getName()); System.out.println(new Person().getName()); System.out.println(new Person("王五").getName()); } } |
张三 无名氏 - 0 无名氏 - 1 李四 无名氏 - 2 王五 |
对于这种代码理解即可,以后如果看到某些系统的程序中存在自动命名的功能,就是如上代码的实现。
2、定义方法,通过方法取得本类的实例化对象
class Person { private String name;
public Person(String name) { this.name = name; }
public String getName() { return this.name; }
public static Person getInstance() { return new Person("默认的名称"); } }
public class Demo { public static void main(String args[]) { System.out.println(new Person("张三").getName()); System.out.println(Person.getInstance().getName()); } } |
张三 默认的名称 |
对于这种操作的取得应该来讲只是以后一个核心程序的一小部分,重点是必须清楚:如果以后类中发现无法使用构造方法的时候,肯定会提供一个static方法供用户使用。
2.2 代码块(了解)
代码块指的是在java之中使用“{}”定义的程序段,根据程序段的位置及所声明的关键字的不同,代码块一共分为四种:普通代码块、构造块、静态代码块、同步代码块。
2.2.1 普通代码块
普通代码块指的是直接在方法或语句中定义的代码块
public static void main(String args[]) { if (true) { int x = 10; System.out.println("x=" + x); } int x = 100; System.out.println("x=" + x); } |
x=10 x=100 |
此时的两个x变量之间并不会互相影响,因为第一个x是定义在if语句之中,属于局部变量,而普通代码块的形式与之类似:
public static void main(String args[]) { { //普通代码块 int x = 10; System.out.println("x=" + x); } int x = 100; System.out.println("x=" + x); } |
x=10 x=100 |
对于普通代码块一般用处不大,知道就行了。
2.2.2 构造块
如果将一个代码块定义在了类之中,则就表示构造块
class Person { { // 类中定义是构造块 System.out.println("A、构造块。"); }
public Person() { System.out.println("B、构造方法。"); } }
public class Demo { public static void main(String args[]) { new Person(); new Person(); } } |
A、构造块。 B、构造方法。 A、构造块。 B、构造方法。 |
通过运行可以发现,构造块优先于构造方法执行,而且每一个新的实例化对象产生,都会重复执行构造块,但是构造块在开发之中基本上是不可能使用,知道就行了。
2.2.3 静态块
静态块指的就是使用static关键字定义的代码块,但是静态块的定义要考虑两种情况:
情况一:在非主类中定义静态类
class Person { static { System.out.println("X、静态块"); } { // 类中定义是构造块 System.out.println("A、构造块。"); }
public Person() { System.out.println("B、构造方法。"); } }
public class Demo { public static void main(String args[]) { new Person(); new Person(); } } |
X、静态块 A、构造块。 B、构造方法。 A、构造块。 B、构造方法。 |
通过运行可以发现,静态块优先于构造块执行,而且不管有多个新对象产生,静态块只执行一次,静态块的主要功能是为静态属性初始化的。
情况二:在主类中定义静态块
public class Demo { static { System.out.println("静态块的输出。"); }
public static void main(String args[]) { System.out.println("主方法中的输出。"); } } |
静态块的输出。 主方法中的输出。 |
通过运行可以发现,静态块的执行优先于主方法执行,而这个时候就可以利用静态块来取代主方法;
public class Demo { static { System.out.println("Hello World."); System.exit(1); // 程序直接退出 } } |
错误: 在类 course_2.Demo 中找不到主方法, 请将主方法定义为: public static void main(String[] args) |
注意,可能有些JDK版本可以无主方法,而直接输出“Hello World.”。
所以一定要记住,主方法并非无可替代,可以使用静态块替代,但是这种程序只是一个娱乐,实际中使用不了。
2.3 思考题
模拟一个用户登录程序,例如登录的操作类是login,通过初始化参数传递用户名和密码;
例如:程序运行格式:java Login 用户名 密码;
默认的用户名和密码:mldn hello
要求:考虑类的设计结构
范例:直接在主方法中编写基本实现
public class Demo { public static void main(String args[]) { String username = args[0]; // 取得第一个参数 String password = args[1]; // 取得第二个参数 if ("mldn".equals(username) && "hello".equals(password)) { System.out.println("用户登录成功"); } else { System.out.println("用户登录不成功"); } } } |
java Demo mldn hello |
用户登录成功 |
此时已经实现了程序的基本功能,而且在现在的程序中存在以下的几个问题:
● 问题一:主方法中的代码过多;
● 问题二:如果输入的参数个数不是两个呢?
那么下面进行问题的解决,思考:打卡流程:
● 保证输入的内容正确:伸出手指头放进扫描区;
● 进行验证:一个专门匹配指纹库的系统进行指纹的验证;
● 显示结果:验证的结果通过指纹的机器进行显示;
可以设计出三个操作类来。
程序一:进行数据验证
class LoginCheck { // 负责验证 private String username; private String password;
public LoginCheck(String username,String password) { this.username = username; this.password = password; }
public boolean isLogin() { if ("mldn".equals(username) && "hello".equals(password)) { return true; } return false; } } |
本类不负责数据的接收,只是根据数据执行相关的验证操作。
程序二:进行操作体的编写
class Operate { private String args[] = null;
public Operate(String args[]) { this.args = args; if (this.args.length != 2) { // 输入的参数不是两个 System.out.println("输入的参数错误!"); System.exit(1); // 程序退出 } }
public String getInfo() { if (new LoginCheck(this.args[0], this.args[1]).isLogin()) { return "用户登录成功!"; } else { return "用户登录失败!"; } } } |
本类只是负责连接输入操作和验证程序,下面使用主程序输入内容:
public class Demo { public static void main(String args[]) { System.out.println(new Operate(args).getInfo()); } } |
java Demo mldn hello |
用户登录成功! |
客户端程序开发之中肯定越少越好;
对于类的设计而言,肯定不是一蹴而就的,需要长时间代码积累,而且类的设计和表的设计一样,只有相对合理的解决方案。
2.4 内部类(重点)
对于内部类的操作而言,如果在几年前基本上是使用不到,但是这几年的技术发展,在很多地方上都开始更多的应用内部类操作了,像Spring框架,或者是以后使用的Android开发等等都有内部类的应用。
2.4.1 内部类的基本概念
所谓的内部类指的就是一个类的内部又定义了其他的操作类的形式,例如,如下的代码:
class Outer { // 定义类 private String name = "Hello";// 外部类的私有属性
class Inner { // 定义内部类 public void print() { System.out.println(name); } }
public void fun() { new Inner().print(); } }
public class Demo { public static void main(String args[]) { new Outer().fun(); } } |
Hello |
如果单独从语法上来讲,内部类的定义操作并不麻烦,但是使用内部类有一个缺点和一个优点:
● 缺点:从java的定义结构上来讲,一个类中只能有属性和方法所组成,此时又增加了一个类,这样的做法会破坏程序的结构;
● 优点:可以方便的访问外部类的私有操作,如果要想进行本概念的验证,则需要将以上的类拆分为两个。
范例:将内部类和外部类拆分
class Outer { private String name = "Hello";
public void fun() { new Inner(this).print(); }
public String getName() { return this.name; } }
class Inner { private Outer out = null;
public Inner(Outer out) { this.out = out; }
public void print() { System.out.println(this.out.getName()); } }
public class Demo { public static void main(String args[]) { new Outer().fun(); } } |
Hello |
此时增加了不少的代码,所以内部类的最方便之处就是可以和外部类之间进行方便的互操作。
既然是类,那么肯定会产生字节码文件,所以下面观察一下内部类的字节码文件:Outer$Inner.class,可以发现现在在内部类的字节码文件中存在了一个“$”符合,而此符合在程序之中会被自动的替换为:“.”那么通过这样的替换也就发现,内部类的完整名称应该是“外部类.内部类”,所以此时,如果要想在外部进行内部类的对象实例化,则格式如下:
外部类.内部类内部类对象 = new 外部类( ) . new 内部类( ) ; |
内部类有可能操作外部类中的普通属性,而普通属性是在对象实例化之后才会被分配空间的,所以就证明在实例化内部类的对象之前,必须先有外部类的实例化对象。
class Outer { // 定义类 private String name = "Hello";// 外部类的私有属性
class Inner { // 定义内部类 public void print() { System.out.println(name); } } }
public class Demo { public static void main(String args[]) { Outer.Inner in = new Outer().new Inner(); in.print(); } } |
Hello |
以上完成了内部类的基本开发,但是在定义内部类的时候有以下三点说明:
说明一:如果现在一个内部类只希望被外部类所操作,则可以在类的定义上使用private声明
class Outer { // 定义类 private String name = "Hello";// 外部类的私有属性
private class Inner { // 定义内部类 public void print() { System.out.println(name); } } } |
说明二:对于类中属性的访问都应该在前面加上“this”关键字,但是以上的程序并没有使用“this.name”,因为如果加上则表示访问的是当前类中的属性,但是如果访问的属性是外部类的话,则应该使用“外部类.this”表示。
class Outer { // 定义类 private String name = "Hello";// 外部类的私有属性
class Inner { // 定义内部类 public void print() { System.out.println(Outer.this.name); } } } |
以后在进行Android开发的时候,这种语法基本上都会一直见到。
说明三:内部类除了可以方便的访问外部类的私有操作,外部类也能方便的访问内部类的私有操作
class Outer { // 定义类 private String name = "Hello";// 外部类的私有属性
class Inner { // 定义内部类 private String var = "World";
public void print() { System.out.println(Outer.this.name); } }
public void fun() { new Inner().print(); System.out.println(new Inner().var); // 直接访问私有 } }
public class Demo { public static void main(String args[]) { new Outer().fun(); } } |
Hello World |
以上的这一特征,完全可以在链表中实现。
2.4.2 使用static定义内部类
内部类也可以使用static关键字进行定义,但是一旦使用了static定义内部类的话,则表示这个内部类就变成了外部类,而内部类的完整名称也就变为“外部类.内部类”,而且此时也只能访问外部类中的static操作了。
class Outer { // 定义类 private static String name = "Hello";// 外部类的私有属性
static class Inner { // 定义外部类 public void print() { System.out.println(Outer.name); } } }
public class Demo { public static void main(String args[]) { Outer.Inner in = new Outer.Inner(); in.print(); } } |
Hello |
注意:使用static定义类的形式,只适合于出现在内部类上,外部类是无法使用的。
2.4.3 在方法中定义内部类
理论上讲,内部类可以在类的任意位置上进行定义,包括代码块之中都可以,可是这之中以方法定义内部类的形式出现的是最多的,所以下面直接在方法中定义内部类。
范例:在方法中定义内部类
class Outer { // 定义类 private static String name = "Hello";// 外部类的私有属性
public void fun() { class Inner { public void print() { System.out.println(Outer.this.name); } } new Inner().print(); } }
public class Demo { public static void main(String args[]) { new Outer().fun(); } } |
Hello |
另外,如果在一个方法中定义的内部类,要想访问方法中的参数或者是变量,则参数或变量前必须使用“final”关键字定义。
class Outer { private static String name = "Hello";
public void fun(final int i) {// 参数 final String y = "World"; // 变量 // static String xi = "wolex"; --错误!方法内不允许定义static属性。 class Inner { public void print() { System.out.println(Outer.this.name); System.out.println(i); System.out.println(y); } } new Inner().print(); } }
public class Demo { public static void main(String args[]) { new Outer().fun(10); } } |
Hello 10 World |
对于内部类的所以操作语法,必须熟悉。
2.5 链表的内部类实现(理解)
在链表程序之中,Node节点类是Link类自己所需要的,很明显如果按照之前的编写,现在肯定Node类可以被任意的类所使用,这样也不方便,所以使用内部类修改。
package course_2;
class Link { // 表示链表的操作类,主要就是操作Node类 private Node root; // 将根节点定义为类中的属性
private class Node { private String data; // 要包装的数据 private Node next; // 保存它的下一个节点
public Node(String data) { // Node类的功能一定要包装数据 this.data = data; }
public void addNode(Node newNode) { // 将节点保存在合适的位置上 if (this.next == null) { // 当前节点后没有其他节点 this.next = newNode; // 保存 } else { // 当前节点之后有内容 this.next.addNode(newNode); // 当前节点的下一个节点保存 } }
public void printNode() { System.out.println(this.data); // 输出当前节点的数据 if (this.next != null) { // 还有后面的节点 this.next.printNode(); } }
public boolean containsNode(String data) { if (this.data.equals(data)) { // 当前节点的数据为查找数据 return true; } else { // 当前节点的数据不是查找数据,继续向下 if (this.next != null) { // 现在没有查找到最后的节点 return this.next.containsNode(data); } else { // 后面没有节点 return false; } } }
public void removeNode(Node previous, String data) { if (this.data.equals(data)) { // 当前 节点的数据满足删除 previous.next = this.next; // 空出当前节点 } else { this.next.removeNode(this, data); // 向下继续删除 } } }
public void add(String data) { // 设置要增加的数据 if (data == null) { // 如果没有数据 return; // 返回到被调用处 } Node newNode = new Node(data); // 将数据包装在节点 if (this.root == null) { // 现在没有根节点 this.root = newNode; // 第一个节点作为根节点 } else { // 如果不是根节点,则通过Node类指定保存的位置 this.root.addNode(newNode); } }
public void print() { if (this.root != null) { // 现在有节点有数据,可以保存 this.root.printNode(); // 输出还是交给Node } }
public boolean contains(String data) { if (data == null || this.root == null) { return false; // 没有内容就不查 } return this.root.containsNode(data);// 交给Node类完成 }
public void remove(String data) { if (this.contains(data)) { // 要删除存在的节点 if (this.root.data.equals(data)) { // 如果删除的是根节点 this.root = this.root.next; // 根节点的下一个节点为根 } else { // 交给Node类完成 // 从根节点的下一个节点开始,判断要删除的节点 this.root.next.removeNode(this.root, data); } } } }
public class Demo { public static void main(String args[]) { Link all = new Link(); all.add("A"); all.add("B"); all.add("C"); all.remove("B"); all.remove("A"); all.print(); System.out.println(all.contains("A")); System.out.println(all.contains("X")); }
} |
C false false |
这种实现唯有的好处就在于减少了程序中的getter方法的使用,这样也可以让Node类只为Link类提供使用。
2.6 继承性(重点)
继承性作为面向对象的第二大特性,其核心表示的含义是:继承已有类的功能继续使用。
2.6.1 继承的引出
为了更好的理解继承的作用,下面通过一个简单的程序来观察,例如,现在要求定义两个类:Person、另外一个是Student,按照之前的做法,现在的代码开发如下:
Person.java | Student.java |
class Person { private String name; private int age;
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
public String getName() { return this.name; }
public int getAge() { return this.age; } } | class Student { private String name; private int age; private String school;
public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public void setSchool(String school) { this.school = school; } public String getName() { return this.name; } public int getAge() { return this.age; } public String getSchool() { return this.school; } } |
通过程序的运行,可以发现现在里面存在了大量的重复代码,但是现在抛开程序不谈,学生和人的关系是:学生就是一个人,但是学生类比人类的定义更加严格,此时按照之前的概念肯定无法解决问题。
2.6.2 继承的实现
在java中,如果想明确的实现继承的关系,则可以使用extends关键字完成,格式如下:
class 子类 extends 父类{
}
但是在这里需要注意的是:
● 子类又被称为派生类;
● 父类也被称为超类(SuperClass)
范例:观察继承的操作
class Person { private String name; private int age;
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
public String getName() { return this.name; }
public int getAge() { return this.age; } }
class Student extends Person { // 学生是人的子类 // 现在的学生类之中没有定义任何的新操作 }
public class Demo { public static void main(String args[]) { Student stu = new Student(); stu.setName("张三"); // 直接使用继承来的方法 stu.setAge(20); // 继承来的方法 System.out.println(stu.getName() + "-->" + stu.getAge());// 继承而来 } } |
张三-->20 |
通过程序的运行可以发现,现在在子类之中,没有定义任何的方法,而是可以直接将父类中定义的操作继续使用,所以证明在子类之中可以对父类进行重用,当然,子类也可以只属于自己的操作。
class Person { private String name; private int age;
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
public String getName() { return this.name; }
public int getAge() { return this.age; } }
class Student extends Person { // 学生是人的子类 private String school; // 子类自己的属性
public void setSchool(String school) { // 子类扩充的方法 this.school = school; }
public String getSchool() { // 子类扩充的方法 return this.school; } }
public class Demo { public static void main(String args[]) { Student stu = new Student(); stu.setName("张三"); // 直接使用继承来的方法 stu.setAge(20); // 继承来的方法 stu.setSchool("清华大学"); // 子类自己扩充的方法 System.out.println(stu.getName() + "-->" + stu.getAge() + "," + stu.getSchool());// 继承而来 } } |
张三-->20,清华大学 |
通过以上的程序可以发现,在开发语言之中,子类最基本的功能也是维持父类的原本操作,所以在程序之中并不会存在所谓的现实生活中的“败家子”的概念。
所以通过程序可以发现,继承的功能就是对已有类功能的一种扩充。
2.6.3 继承的限制
对于继承,实际上一旦使用会带来许多的注意事项。
注意事项一:在java之中不允许多重继承,即一个子类只能继承一个父类
不允许多重继承,但是现在的C子类又需要同时继续A和B类,那么可以采用多层继承解决:
多重继承(×): | 多层继承(√): |
Class A {} Class B {} Class C extends A,B{}//同时继承两个父类 | Class A {} Class B extends A{} Class C extends B{} |
使用多层继承之后,C类就同时具备了A类和B类的操作功能。
注意事项二:当子类继承的时候,实际上是将父类的全部内容继承下来,但是有以下两点区别:
● 所有的非私有操作采用显式继承的方式; → 子类可以直接使用
● 所有的私有操作采用隐式继承的方式; → 子类可以间接使用
按照之前的开发所讲,一个类中的全部内容肯定都被子类继承,所以对于之前学生和人之间的操作,学生类也继承了Person类中的name和age两个属性,这两个属性是间接继承(隐式继承),只能通过方法访问;
注意事项三:子类对象在进行实例化操作之前,会默认调用父类中的无参构造方法,为父类属性分配空间,之后再调用子类的构造方法,为本类属性分配空间。
class Person { public Person() { // 无参构造 System.out.println("**Person类的构造。"); } }
class Student extends Person { // 学生是人的子类 public Student() { System.out.println("**Student类的构造。"); } }
public class Demo { public static void main(String args[]) { new Student(); // 实例化子类对象 } } |
**Person类的构造。 **Student类的构造。 |
通过运行可以发现,很明显现在是父类的构造方法先执行,而后再执行子类的构造方法,即,现在对于子类的构造而言就相当于隐藏了一个“super( )”语句。
class Person { public Person() { // 无参构造 System.out.println("**Person类的构造。"); } }
class Student extends Person { // 学生是人的子类 public Student() { super(); System.out.println("**Student类的构造。"); } }
public class Demo { public static void main(String args[]) { new Student(); // 实例化子类对象 } } |
**Person类的构造。 **Student类的构造。 |
而如果说父类中有明确的构造方法,这个时候就可以利用super指定要调用的父类构造参数。
class Person { public Person(String name, int age) { System.out.println("**Person类的构造。"); } }
class Student extends Person { public Student(String name, int age, String school) { super(name, age); //super()必须在第一行 System.out.println("**Student类的构造。"); } }
public class Demo { public static void main(String args[]) { new Student("", 1, ""); // 实例化子类对象 } } |
**Person类的构造。 **Student类的构造。 |
所以通过以上的代码就可以证明出,子类和父类是永远也分不开的,而且不管子类如何操作,最终也都要调用父类中的构造方法,但是这个时候有人会提出疑问了,如果代码换种形式呢?
既然现在的操作是进行构造方法的调用,所以现在肯定“super( )”的语句要放在构造方法的首行上,但是现在不使用super( ),换成this( )。
class Person { public Person() { System.out.println("**Person类的构造。"); } }
class Student extends Person { public Student(String name) { this("", 0, ""); }
public Student(String name, int age) { this(name); }
public Student(String name, int age, String school) { this(name, age); } }
public class Demo { public static void main(String args[]) { new Student("", 1, ""); // 实例化子类对象 } } |
不管环境如何改变,父类构造永远会被子类调用,这也就是在之前讲解this关键字所说的一个问题了:不管如何进行构造方法的互调用,最终肯定有一个构造要作为程序的出口,那么这个出口肯定要调用父类构造。
2.7 覆写(重点)
当继承关系发生之后,对于子类而言很有可能定义了与父类相同的方法或属性,所以这个时候就指会发生覆写的问题,当然覆写分为两种情况讨论:方法的覆写,属性的覆写。
2.7.1 方法的覆写
所谓方法的覆写指的就是子类定义了与父类相同的方法的时候(方法名称相同、参数的类型或个数相同、返回值类型相同)。
class Person { public void say() { System.out.println("我是一个人。"); } }
class Student extends Person { public void say() { // 此时表示方法的覆写 System.out.println("我是一个学生。"); } }
public class Demo { public static void main(String args[]) { new Student().say(); } } |
我是一个学生。 |
现在实例化的是子类对象,所以所操作的方法肯定是被子类所覆写过的方法。
但是在进行方法的覆写的时候也需要注意权限的问题,对于权限现在一共学习过三种:
● public > default(什么都不写)> private;
被子类所覆写的方法不能拥有比父类更严格的访问控制权限。
● 例如:父类中的方法是default权限,子类中的方法权限:public、default;
● 例如:父类中的方法是public权限,子类中的方法权限:public
Note:这个结合生活也很好理解,即父亲有一笔estate,他向社会公开(即public)数目地传给儿子,此时儿子是不可能再将其变为秘密(即只有自己知道,private),因为父亲(即父类)已经公之于众;On the contrary,如果父亲是秘密地(private)传给儿子,此时社会并不知道,儿子是有权将其公开(public)或者保留父亲意愿将其作为秘密(private)。
就开发而已,在开发之中,基本上的方法90%都是使用public权限定义的。
思考题:如果现在父类中的方法权限是private,子类的方法权限是default叫覆写吗?
● 首先从它的描述上可以发现,权限由private变为default,属于扩大权限;
class Person { private void say() { System.out.println("我是一个人。"); }
public void fun() { this.say(); } }
class Student extends Person { void say() { // 此时表示方法的覆写 System.out.println("我是一个学生。"); } }
public class Demo { public static void main(String args[]) { new Student().fun(); } } |
我是一个人。 |
通过验证可以发现,如果父类的方法定义成private访问权限,则子类根本就不可能覆写,而在实际的开发之中,此类问题也不会出现,因为只要是方法90%都是public。
如果现在一个子类覆写了父类的某个方法,而且又希望在子类中调用父类中被子类所覆写过的方法,可以使用“super.方法()”的形式完成。
class Person { void say() { System.out.println("我是一个人。"); }
}
class Student extends Person { void say() { // 此时表示方法的覆写 super.say(); // 直接找到父类中的指定方法 System.out.println("我是一个学生。"); } }
public class Demo { public static void main(String args[]) { new Student().say(); } } |
我是一个人。 我是一个学生。 |
同时也需要注意一点,关于this和super的操作范围:
● this.方法( )指的是调用本类中的方法,如果本类中不存在此方法在从父类中查找使用;
● super.方法( )指的是直接调用父类中的方法,不再查找子类。
建议:如果以后在编写程序的时候,如果调用的是父类方法最好加上“super”。
2.7.2 属性的覆盖(了解)
当一个子类的属性名称与父类的属性名称相同(不需要类型相同)的时候就表示发生了属性的覆写,下面通过代码演示一下。
class Person { public int msg = 100; //如果改为private,则子类不能覆写 }
class Student extends Person { private String msg = "Hello"; // 属性名称相同
public void say() { System.out.println("msg = " + this.msg); System.out.println("msg = " + super.msg); } }
public class Demo { public static void main(String args[]) { new Student().say(); } } |
msg = Hello msg = 100 |
但是这种代码根本就没有任何意义,因为所以的属性都必须封装,封装之后的就无法覆写了。
2.8 两个重要的概念区别(重点)
学习完了以上的知识点之后,现在就存在了两个区别:方法的重载与覆写的区别、this和super的区别;
2.8.1 区别:方法的重载与覆写
No. | 区别 | 重载 | 覆写 |
1 | 单词 | Overloading | Override |
2 | 概念 | 方法名称相同,参数的类型或个数不同 | 方法名称、返回值类型、参数的个数及类型全部相同 |
3 | 范围 | 发生在一个类之中 | 发生在继承关系中 |
4 | 权限 | 重载的时候没有权限的限制 | 子类覆写的方法不能拥有比父类更严格的访问控制权限 |
面试题:请解释方法重载和覆写的区别,并且当重载的时候是否可以改变方法的返回值类型?
所有的区别就使用如上的表格回答;重载的时候返回值类型可以不相同,但是没有意义。
2.8.2 区别:this和super
No | 区别 | this | super |
1 | 属性 | this.属性指的是找到本类的属性,如果本类没有找到则继续查找父类 | super.属性直接在子类之中查找父类中的指定属性,不再查找子类本身 |
2 | 方法 | this.方法()指的是找到本类的方法,如果本类没有找到则继续查找父类 | super.方法()直接在子类之中查找父类中的指定方法,不再查找子类本身 |
3 | 构造 | 都必须放在构造方法的首行,所以两个操作不能同时出现 | |
4 | 特殊 | 表示当前对象 |
|
建议:为了方便开发的维护,以后要使用this和super明确的划分出子类和父类的操作。
2.9 核心思考题(重点)
现在要求定义一个整形数组的操作类,数组的大小由外部决定,用户可以向数组之中增加数据,以及取得数组中的全部数据,也可以根据外部提供的数组的增加大小,在原本的数组之上扩充指定的容量,另外,在此类上派生两个子类:
● 排序类:取得的数组内容是经过排序出来的结果;
● 反转类:取得的数组内容是反转出来的结果;
提示:在完成程序的时候根本就没有必要去考虑子类的问题,先完成一个父类。
范例:先开发父类 —— Array
class Array { // 操作数组 private int data[] = null; // 要操作的是整形数组 private int foot = 0; // 增加数据的脚标
public Array(int len) { if (len > 0) { this.data = new int[len];// 开辟新数组 } else { this.data = new int[1]; // 维持一个长度 } }
public void increment(int len) { // 可扩充容量 int[] newArr = new int[this.data.length + len]; // 开辟新数组 System.arraycopy(this.data, 0, newArr, 0, this.data.length); this.data = newArr; }
public boolean add(int num) { if (this.foot < data.length) { // 还有地方增加数据 this.data[this.foot++] = num;// 保存数据 return true; } return false; }
public int[] getData() { return this.data; } }
public class Thought { public static void main(String args[]) { Array arr = new Array(5); System.out.println(arr.add(21)); System.out.println(arr.add(87)); System.out.println(arr.add(43)); System.out.println(arr.add(12)); System.out.println(arr.add(84)); System.out.println(arr.add(6)); arr.increment(3); System.out.println(arr.add(6)); System.out.println(arr.add(7)); System.out.println(arr.add(8)); int temp[] = arr.getData(); for (int i = 0; i < temp.length; i++) { System.out.print(temp[i] + "、"); } } } |
true true true true true false true true true 21、87、43、12、84、6、7、8、 |
范例:开发排序类 —— SortArray
class Array { // 操作数组 private int data[] = null; // 要操作的是整形数组 private int foot = 0; // 增加数据的脚标
public Array(int len) { if (len > 0) { this.data = new int[len];// 开辟新数组 } else { this.data = new int[1]; // 维持一个长度 } }
public void increment(int len) { // 可扩充容量 int[] newArr = new int[this.data.length + len]; // 开辟新数组 System.arraycopy(this.data, 0, newArr, 0, this.data.length); this.data = newArr; }
public boolean add(int num) { if (this.foot < data.length) { // 还有地方增加数据 this.data[this.foot++] = num;// 保存数据 return true; } return false; }
public int[] getData() { return this.data; } }
class SortArray extends Array { public SortArray(int len) { super(len); }
public int[] getData() { java.util.Arrays.sort(super.getData()); return super.getData(); } }
public class Thought { public static void main(String args[]) { SortArray arr = new SortArray(5); System.out.println(arr.add(21)); System.out.println(arr.add(87)); System.out.println(arr.add(43)); System.out.println(arr.add(12)); System.out.println(arr.add(84)); System.out.println(arr.add(6)); arr.increment(3); System.out.println(arr.add(16)); System.out.println(arr.add(7)); System.out.println(arr.add(58)); int temp[] = arr.getData(); for (int i = 0; i < temp.length; i++) { System.out.print(temp[i] + "、"); } } } |
true true true true true false true true true 7、12、16、21、43、58、84、87、 |
范例:开发反转类 —— ReverseArray
class Array { // 操作数组 private int data[] = null; // 要操作的是整形数组 private int foot = 0; // 增加数据的脚标
public Array(int len) { if (len > 0) { this.data = new int[len];// 开辟新数组 } else { this.data = new int[1]; // 维持一个长度 } }
public void increment(int len) { // 可扩充容量 int[] newArr = new int[this.data.length + len]; // 开辟新数组 System.arraycopy(this.data, 0, newArr, 0, this.data.length); this.data = newArr; }
public boolean add(int num) { if (this.foot < data.length) { // 还有地方增加数据 this.data[this.foot++] = num;// 保存数据 return true; } return false; }
public int[] getData() { return this.data; } }
class SortArray extends Array { public SortArray(int len) { super(len); }
public int[] getData() { java.util.Arrays.sort(super.getData()); return super.getData(); } }
class ReverseArray extends Array { public ReverseArray(int len) { super(len); }
public int[] getData() { int center = super.getData().length / 2; int head = 0; int tail = super.getData().length - 1; for (int i = 0; i < center; i++) { int temp = super.getData()[head]; super.getData()[head] = super.getData()[tail]; super.getData()[tail] = temp; head++; tail--; } return super.getData(); } }
public class Thought { public static void main(String args[]) { ReverseArray arr = new ReverseArray(5); System.out.println(arr.add(1)); System.out.println(arr.add(2)); System.out.println(arr.add(3)); System.out.println(arr.add(4)); System.out.println(arr.add(5)); System.out.println(arr.add(6)); arr.increment(3); System.out.println(arr.add(6)); System.out.println(arr.add(7)); System.out.println(arr.add(8)); int temp[] = arr.getData(); for (int i = 0; i < temp.length; i++) { System.out.print(temp[i] + "、"); } } } |
true true true true true false true true true 8、7、6、5、4、3、2、1、 |
本程序之中融合数组的操作,以及继承中的各个操作概念,还有覆写的应用。
3、总结
1、在进行继承开发的时候,重点的部分放在父类上完成,子类可以根据自己的需要选择对操作方法进行覆写;
2、static声明的属性和方法,可以在没有实例化对象的时候进行调用;
3、内部类的操作及特点;
4、继承性和方法的覆写是一个极为重要的概念组合,一定要记住一句话:“使用哪个子类就使用被这个子类所覆写的方法”;