JavaSE学习总结(六)面向对象(中)代码块/继承/this和super/父类没有无参构造子类怎么办/方法重写/重写和重载的区别/final关键字/多态/多态成员访问特点/向上转型向下转型/孔子装爹
面向对象(中)
一、代码块
(一)代码块概述
在Java中,使用{}
括起来的代码被称为代码块。
(二)代码块分类
根据其位置和声明的不同,可以分为局部代码块、构造代码块、静态代码块和同步代码块(多线程部分讲解)。
(三)常见代码块的应用
1.局部代码块
在方法中出现;限定变量生命周期,及早释放,提高内存利用率
2.构造代码块
在类中方法外出现;多个构造方法方法中相同的代码存放到一起,每次调用构造方法都执行,并且在构造方法前执行
3.静态代码块
在类中方法外出现,并加上static修饰;静态代码块用于给类进行初始化,随着类的加载而加载,优先于构造代码块和局部代码块执行,且由于类只加载了一次,因此静态代码块只执行一次。静态代码块中只能访问静态变量。
案例演示1
public class MyTest1 {
public static void main(String[] args) {
//局部代码块:定义在方法中的代码块
int b=100;
{
//局部代码块,可以尽早的释放空间和资源
int a=200;
System.out.println("这是一个局部代码块");
System.out.println(a);
System.out.println(b);
}
//System.out.println(a);//报错!当局部代码块的右括号结束后,括号内的资源全部释放,因此变量a不再存在
System.out.println(b);
}
}
案例演示2
class Student{
static {
System.out.println("Student 静态代码块"); //3
}
{
System.out.println("Student 构造代码块"); //4 6
}
public Student() {
System.out.println("Student 构造方法"); //5 7
}
}
class StudentDemo {
static {
System.out.println("StudentDemo的静态代码块");//1
}
public static void main(String[] args) {
System.out.println("我是main方法");//2
Student s1 = new Student();
Student s2 = new Student();
}
}
二、继承
(一)继承概述
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。
(二)继承格式
通过extends
关键字可以实现类与类的继承
class 子类名 extends 父类名 {}
1
单独的这个类称为父类,基类或者超类;继承该类的这多个类可以称为子类或者派生类。
那么什么时候可以使用继承呢?
继承其实体现的是一种关系:“is a” .
采用假设法:如果有两个类A、B。只要他们符合A是B的一种,或者B是A的一种,就可以考虑使用继承。
案例演示1
public class MyTest {
public static void main(String[] args) {
Zi zi = new Zi();
//子类可以继承父类的成员变量和成员方法,并且可以使用
System.out.println(zi.num);
zi.show();
}
}
class Fu{
int num=100;
public void show(){
System.out.println("这是一个show方法");
}
}
class Zi extends Fu{
}
案例演示2
public class MyTest2 {
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "汤姆";
cat.age = 1;
System.out.println(cat.name);
System.out.println(cat.age);
cat.eat();
cat.sleep();
cat.catchMouse();
System.out.println("----------------------");
Dog dog = new Dog();
dog.name = "旺财";
dog.age = 10;
System.out.println(dog.name);
System.out.println(dog.age);
dog.eat();
dog.sleep();
dog.lookDoor();
}
}
class Animal {
String name;
int age;
public void eat() {
System.out.println("吃饭");
}
public void sleep() {
System.out.println("睡觉");
}
}
//猫和狗都有名字、年龄、吃饭、睡觉等共性,如果在每个类中都写一次就会增加重复代码
//因此我们可以讲这些共性抽取到动物类中,而猫和狗类继承这个动物类,猫和狗类中只需要写自己的特性代码
class Cat extends Animal{
public void catchMouse(){
System.out.println("抓老鼠");
}
}
class Dog extends Animal{
public void lookDoor() {
System.out.println("看门");
}
}
(三)继承的优点和弊端
1.优点:
- 提高了代码的复用性
- 提高了代码的维护性
- 让类与类之间产生了关系,继承是多态的前提
2.弊端:
- 类的耦合性增强了
开发的原则:高内聚,低耦合。
耦合:类与类之间关联的程度(一个类要完成某个功能需要依赖其他类的程度)
内聚:一个类单独完成某个功能的能力
(四)Java中类的继承特点
1.Java只支持单继承,不支持多继承。
有些语言是支持多继承,例如C++,格式:extends 类1,类2,…
2.Java支持多层继承(可以使用多层继承关系中上几层的类的非私有成员)
Object类是所有类的顶层父类,所有类都是直接或间接地继承Object类
(五)继承的注意事项
1.子类只能继承父类所有非私有的成员(成员方法和成员变量)
2.子类不能继承父类的构造方法
但是可以通过super(下边讲)关键字去访问父类构造方法。
3.不要为了部分功能而去继承
会增加耦合性,根据实际情况,如果共同属性和行为不多就不建议继承。
案例演示
public class MyTest3{
public static void main(String[] args) {
C c1 = new C();
System.out.println(c1.a);//C类对象可以使用上两层的A类的非私有变量a
System.out.println(c1.b);
// System.out.println(c1.d);报错:d是B类的私有成员变量
// B b1 = new B();
// b1.show();报错:show()是A类的私有方法
// C c2 = new C();
// c2.show();报错:show()是A类的私有方法
}
}
class A{//默认有个extends Object
int a=100;
private void show(){
System.out.println("这是一个私有的方法");
}
}
class B extends A{
int b=200;
private int d=10;
}
class C extends B{
}
(六)继承中成员变量的“就近原则”
如果子类中的成员变量和父类中的成员变量名称一样,在子类中访问一个该变量的查找顺序遵循"就近原则":
- 在子类的方法的局部范围找,有就使用
- 在子类的成员范围找,有就使用
- 在父类的成员范围找,有就使用
- 如果还找不到,就报错
案例演示
public class MyTest4 {
public static void main(String[] args) {
B b = new B();
int num=1;
b.show(num);
}
}
class A{
int num=100;
}
class B extends A{
int num=200;
public void show(int num){
System.out.println(num); //1
// 变量的访问原则:遵循就近原则,先在局部找这个变量,找到就使用,如果局部没找到,去本类的成员位置找,找到就使用
//如果本类的成员位置没找到,去父类的成员位置找,找到就使用。
System.out.println(this.num);//200 指的是本类对象的成员变量num
System.out.println(new A().num);//100 指的是A类的成员变量num
//System.out.println(super.num);//100 也可以用super的方法,指的是父类的成员变量num,下边讲
}
}
(七)this和super的区别和应用
1.this和super的区别
this 代表的是本类对象的引用
super 代表的是父类存储空间的标识(可以理解成父类的引用,可以操作父类的成员)
2.this和super的应用
- 调用成员变量
this.成员变量
调用本类的成员变量
super.成员变量
调用父类的成员变量 - 调用构造方法
this(...)
调用本类的构造方法
super(...)
调用父类的构造方法 - 调用成员方法
this.成员方法
调用本类的成员方法
super.成员方法
调用父类的成员方法
案例演示
public class MyTest3 {
public static void main(String[] args) {
B b = new B();
b.show(1);
b.test1();
System.out.println("------------");
b.test();
}
}
class A {
int a = 200;
public void test1() {
System.out.println("这是父类的test1方法");
}
}
class B extends A {
int a = 100;
public void show(int a) {
System.out.println(a);
System.out.println(this.a);
System.out.println(super.a);
}
public void test2() {
System.out.println("这是子类的test2方法");
}
public void test() {
this.test2();
this.test1();//本类对象调用父类的方法
super.test1();//使用父类的空间标识去调用父类的方法
}
}
(八)继承中构造方法的关系
注意: 构造方法不参与继承,子类中所有的构造方法默认都会访问父类中空参数的构造方法。
为什么呢?
因为有了继承关系后,子类要去继承父类的数据,可能还会要使用父类的数据,所以肯定先要让父类的构造方法执行,来完成对父类数据的初始化,然后再完成子类的数据的初始化。即初始化子类前,先要完成父类数据的初始化。
其实:
每一个构造方法的第一条语句默认都是:super(),只是没显示出来,也可以自己手动写出来。
案例演示
public class MyTest {
public static void main(String[] args) {
Son son1 = new Son();
System.out.println("-------------");
Son son2 = new Son(102);
}
}
class Father{
int num=1000;
public Father() {
super();
System.out.println("这是父类的空参构造");
}
}
class Son extends Father{
int b=10;
public Son() {
super();
System.out.println("这是子类的空参构造");
}
public Son(int b) {
super();
this.b = b;
System.out.println("这是子类的有参构造");
}
}
注意事项:
1.父类没有无参构造方法,子类怎么办?有以下三种方法:
①子类的构造方法通过super
去显式调用父类其他的带参的构造方法
②子类的构造方法通过this
去调用本类的其他构造方法,但是这里的其他构造方法也必须首先访问了父类的带参构造方法
③在父类中添加一个无参的构造方法
2.super(…)
或者this(….)
必须出现在第一条语句上, 否则,可能会对父类数据进行多次初始化。
案例演示
public class MyTest {
public static void main(String[] args) {
Zi zi = new Zi();
System.out.println("-------------");
Zi zi1 = new Zi(1009);
System.out.println(zi.num);
System.out.println(zi1.num);
}
}
class Fu{
int num=10;
/*注释掉父类的空参构造
public Fu() {
System.out.println("父类的空参构造执行了");
}
*/
public Fu(int num) {
this.num = num;
System.out.println("父类的有参构造执行了");
}
}
class Zi extends Fu{
int num=100;
public Zi() {
super(10); //方法1:通过super去显式调用父类其他的带参的构造方法
System.out.println("子类的空参构造执行了");
}
/*
public Zi(int num) {
super(num);
System.out.println("子类的有参构造执行了");
}
*/
public Zi(int num) {
this();//方法2:通过this去调用本类的其他构造方法,但是这里的其他构造方法也必须首先访问了父类的带参构造方法
System.out.println("子类的有参构造执行了");
}
}
思考题:继承中的代码块执行顺序
public class MyTest{
public static void main(String[] args){
Zi z=new Zi();
}
}
class Fu {
static {
System.out.println("静态代码块Fu");
}
{
System.out.println("构造代码块Fu");
}
public Fu() {
System.out.println("构造方法Fu");
}
}
class Zi extends Fu {
static {
System.out.println("静态代码块Zi");
}
{
System.out.println("构造代码块Zi");
}
public Zi() {
System.out.println("构造方法Zi");
}
}
(九)方法重写
我们知道,当子类的方法名和父类的方法名一样,而参数列表(或者参数列表和返回值类型)不一样的时候,通过子类调用方法:
- 先查找子类中有没有该方法,如果有就使用
- 再看父类中有没有该方法,有就使用
- 如果都没有就报错
那么,如果子类中出现了方法名、参数列表、返回值类型都和父类中一模一样的方法声明,又该怎么办呢?
1.什么是方法重写?
子类中出现了和父类中一模一样的方法声明(方法名,参数列表,返回值类型),也被称为方法覆盖,方法复写。
2.方法重写的应用:
如果说子类对父类的方法实现“不满意”,那么子类就可以覆盖他,或者说,子类想要对父类的方法的实现功能进行扩展,也可以使用方法重写的这种机制。这样,既沿袭了父类的功能,又定义了子类特有的内容。
案例演示
public class MyTest {
public static void main(String[] args) {
new NewPhone().call();
}
}
class Phone{
public void call(){
System.out.println("打电话");
}
}
class NewPhone extends Phone{
@Override//重写的注解 可以检测此方法是否是重写父类的 可加可不加 最好加上
public void call() {//方法重写,新手机不仅可以打电话还能视频
super.call();
System.out.println("视频电话");
}
}
3.Override(方法重写)和Overload(方法重载)的区别?
两者不同主要体现在:
- 目的
overload用于增加程序的可读性(做法不同,但是做的是同一事情)。 override用于提供其父类已经提供的方法的特定实现。 - 范围
overload 在相同的类范围内执行。 override发生在两个具有继承关系的类中。 - 参数列表
overload参数列表必须不同。 override参数列表必须相同。 - 返回值类型
overload中可以相同或不同,但是光是返回值类型不同不构成重载。 override必须是相同的。
4.方法重写注意事项
a:父类中私有方法不能被重写
因为父类私有方法子类根本就无法继承,何谈重写
b:子类重写父类方法时,访问权限不能更低
要比父类的高或者一样,最好一样
public>protected>缺省的>private
c:静态方法只能被继承,不能被重写
如果子类有和父类相同的静态方法,子类调用这个方法时,调用的是子类的静态方法,但这并不是重写的原因,而是父类的静态方法被隐藏了,对于子类不可见,也就是说,子类和父类中相同的静态方法是没有关系的方法,他们的行为不具有多态性。但是父类的静态方法可以通过父类.方法名
调用。
(十)final关键字
1.为什么会有final
由于继承中有一个方法重写的机制,而有时候我们不想让子类去重写父类的方法,针对这种情况java就给我们提供了一个关键字: final
2.final概述
final关键字是最终的意思,可以修饰类、变量、成员方法、对象。
3.final修饰特点
修饰类: 被修饰的类不能被继承
修饰方法: 被修饰的方法可以被继承,但是不能被重写
修饰变量: 被修饰的变量不能被重新赋值,因为这个变量被修饰后会变成常量
修饰对象:被修饰的对象,内容可变,引用不可变。
4.final修饰局部变量
局部变量如果是基本类型,值不能被改变
局部变量如果是引用类型,地址值不能被改变
案例演示
public class MyTest {
public static final int A=10; //公共的静态常量
public static void main(String[] args) {
System.out.println(A);
System.out.println(MyTest.A);
new Zi().test();//可以继承被final修饰的方法,但不能重写
new Zi().show();
//final 修饰基本数据类型,指的是这个值不能再次被改变,此变量为常量
final int NUM=100;
System.out.println(NUM);
//NUM=99;报错
//final 修饰引用数据类型,指的是这个地址值,不能再次被改变
final Fu fu = new Fu();
System.out.println(fu);
//fu=new Fu();报错
}
}
final class C{
}
/*报错,不能继承final修饰的类
class D extends C{
}
*/
class Fu{
public void show(){
System.out.println("这是父类的show");
}
public final void test(){
System.out.println("这是父类的final-test");
}
}
class Zi extends Fu{
@Override
public void show() {
System.out.println("这是子类的show");
}
}
三、多态
(一)多态概述
某一个事物,在不同时刻表现出来的不同状态。
举例:
家猫可以是猫类的对象,同时家猫也是动物的一种,也可以把家猫称为动物。
Cat c=new Cat();
Animal c=new Cat();
(二)多态前提条件
1.要有继承关系。
2.要有方法重写。 其实没有也是可以的,但是如果没有这个就没有意义。
3.要有父类引用指向子类对象。
父 f = new 子();
案例演示1
public class MyTest {
public static void main(String[] args) {
//多态,父类引用指向的是子类对象
Animal an=new Cat();
an.sleep();
an.eat();
an.show();
}
}
class Animal{
public void eat(){
System.out.println("吃饭");
}
public void sleep() {
System.out.println("睡觉");
}
public void show() {
System.out.println("父类中的show方法");
}
}
class Cat extends Animal{
@Override
public void eat() {
System.out.println("猫爱吃鱼");
}
@Override
public void sleep() {
System.out.println("猫爱白天睡觉");
}
}
那么这个多态到底有什么实际意义呢,再看这个案例。
案例演示2
public class MyTest {
public static void main(String[] args) {
Cat cat = new Cat();
MyUtils.testEat(cat);
Dog dog = new Dog();
MyUtils.testEat(dog);
Rabbit rabbit = new Rabbit();
MyUtils.testEat(rabbit);
}
}
class Animal{
public void eat(){
System.out.println("吃饭");
}
}
class Cat extends Animal{
@Override
public void eat() {
System.out.println("猫吃小鱼干");
}
}
class Dog extends Animal{
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
class Rabbit extends Animal{
@Override
public void eat() {
System.out.println("兔子吃草");
}
}
//工具类
class MyUtils{
public static void testEat(Cat cat){//静态方法不需要创建对象使用,直接拿MyUtils类调用
cat.eat();
}
public static void testEat(Dog dog){
dog.eat();
}
public static void testEat(Rabbit rabbit){
rabbit.eat();
}
}
从案例2看出,当我们每创建一种动物类的对象,在工具类中就需要同步声明一个方法。比如我要是再创建一个老虎类Tiger
,并且创建老虎类的对象,同时想通过工具类调用它重写的eat
方法,那么我必须在MyUtils类中再声明一个public static void testEat(Tiger tiger){...}
,这样就十分麻烦,要是还有上百种动物类,那工程量可想而知。因此,我们就需要运用到多态的机制,MyUtils中只需要一个方法声明就能搞定所有动物。
class MyUtils{
public static void testEat(Animal an){//例如当传进来是个猫类时,相当于Animal an=new Cat(),父类引用指向子类对象
an.eat();
}
}
(三)多态中的成员访问特点
1.成员变量
如果子类和父类变量重名,使用时也是使用的父类的变量。
2.成员方法
如果子类重写了父类的方法,那么调用时使用子类重写的方法,否则,使用父类的方法。
案例演示
public class MyTest {
public static void main(String[] args) {
Fu fu = new Zi();//多态,父类引用指向子类对象
System.out.println(fu.num);//用的还是父类的变量
fu.show();//用的是子类重写后的方法
}
}
class Fu{
int num = 100;
public void show(){
System.out.println("父类的show方法");
}
}
class Zi extends Fu{
int num=10;
@Override
public void show() {
System.out.println("子类重写过后的show方法");
}
}
思考题:写出结果
class A {
public void show() {
show2();
}
public void show2() {
System.out.println("我");
}
}
class B extends A {
public void show2() {
System.out.println("爱");
}
}
class C extends B {
public void show() {
super.show();
}
public void show2() {
System.out.println("你");
}
}
public class MyTest {
public static void main(String[] args) {
A a = new B();
a.show();
B b = new C();
b.show();
}
}
解析:首先,从main方法看起,A a = new B(); a.show();
执行完这两句后,先去B类找是否有show方法的重写,结果没有,那就使用A类的show方法,由于它调用了show2方法,那么再去看B类有没有show2方法的重写,结果有,就执行,输出“爱”。B b = new C(); b.show();
接着,执行这两句,先去C类找是否有show方法的重写,结果有,但它的重写调用了父类的show方法,而父类B没有show方法,那再找上一层A类的show方法,找到后执行,由于它调用了show2方法,那么再去看C类有没有show2方法的重写,结果有,就执行,输出“你”。
结果:
3.静态方法
一个指向子类对象的父类引用来调用父子同名的静态方法时,只会调用父类的静态方法。
案例演示
public class MyTest {
public static void main(String[] args) {
Fu fu = new Zi();
fu.show();//多态情况下,调用父子同名的静态方法,只会调用父类的
//非多态情况下,子类调用父子同名的静态方法,是调用子类的,但这不是重写,而是因为这种情况父类的静态方法对子类隐藏了
Zi zi = new Zi();
zi.show();
Fu fu1 = new Fu();
fu1.show();
}
}
class Fu{
public static void show(){
System.out.println("父类的静态方法");
}
}
class Zi extends Fu{
public static void show() {
System.out.println("子类的静态方法");
}
}
(四)多态的优点和弊端
1.优点
- 提高了代码的维护性(继承保证)
- 提高了代码的扩展性(由多态保证)
2.弊端
- 不能访问子类特有的成员变量和成员方法
解决方法:把父类的引用强制转换为子类的引用。(向下转型)
案例演示
public class MyTest {
public static void main(String[] args) {
Fu fu = new Zi();
System.out.println(fu.a);
//System.out.println(fu.num);报错,多态中无法访问子类独有的成员变量
//fu.test();报错,多态中无法访问子类独有的成员方法
//那么,我们就想要访问子类独有的成员怎么办呢?我们可以向下转型
Zi zi= (Zi) fu; //向下转型 将Fu类的对象fu强行向下转型为Zi类对象zi
System.out.println(zi.num);
zi.test();
}
}
class Fu{
int a=10;
}
class Zi extends Fu{
int num=20;
public void test(){
System.out.println("子类一个特有的方法");
}
}
有了向下转型,那么有没有向上转型呢?
其实,多态就是向上转型。父 f = new 子();
为了更好地理解向上转型和向下转型,再举个例子:孔子装爹
纯属瞎编:有这么个故事,孔子的爹擅长Java编程教学,因此很多人慕名而来
有一天,一位学者请孔子的爹到家中去讲授JavaSE,留孔子一个人在家
过了一会,又有一位学者来到孔子家中想请教孔子的爹,但孔子爹已经出门,而孔子不想失去这个学生
孔子就起了私心,粘上了胡子,穿上了他爹的服装,开始装爹,给这位学者讲授论语
当讲授完后,孔子卸下了伪装,做回自己,跑去打王者荣耀了
public class MyTest{
public static void main(String[] args) {
孔子爹 k = new 孔子();//向上转型(多态):孔子装爹
System.out.println(k.age);//此时看上去的年龄
k.teach();//孔子讲授论语
孔子 k1=(孔子) k;//向下转型:做回自己
System.out.println(k1.age);//此时看上去的年龄
k1.playGame();//做孔子独有的爱好:打王者荣耀
}
}
class 孔子爹{
int age=60;
public void teach(){
System.out.println("讲授Java");
}
}
class 孔子 extends 孔子爹{
int age=30;
@Override
public void teach() {
System.out.println("讲授论语");
}
public void playGame(){
System.out.println("玩王者荣耀");
}
}
声明:本例只是为了更好地理解向上转型和向下转型,但是用中文给类命名实在不推荐,请勿模仿。
(五)多态的内存图解
首先,MyTest.class
加载进方法区,将main()
加载进栈执行,当执行到Fu fu=new Zi();
时,在堆内存中创建Zi对象的内存空间,然而我们首先要对Fu类进行初始化,但是这句话没有创建Fu类的对象,那么Fu类的内存空间就创建在Zi类当中,此时Fu类的引用指向的是Zi类空间中的super的空间。将Fu类的变量num默认初始化为0,再显式初始化为100,然后将Zi类的变量num默认初始化为0,再显式初始化为200。由于引用fu指向super的空间,因此fu.num
输出的是100。接着执行fu.show();
,由于子类重写了show方法,所以它动态绑定到子类的show方法并调入进栈执行,输出“子类的show”,弹栈。然后Zi zi=(Zi) fu;
向下转型,这时引用zi指向Zi类的this空间,因此zi.num
输出的是200,zi.test();
输出“子类的test”,执行完弹栈。最后main()
弹栈。