一、特征一:封装
封装介绍
为什么要封装?封装的含义与作用?
举个例子,就像我们开车,车也分手动挡,自动挡,手动挡的话我们是不是起步需要挂一档,后面随着速度换不同的挡数;自动挡只要你踩踩油门就行,手动挡与自动挡都进行了封装,只不过自动挡封装的更好。一般来说不需要你去了解内部构造,也不想让你去修改内部构造。
隐藏对象内部的复杂性,只对外公开简单的接口,便于外界调用。从而提高系统的可扩展性、可维护性。简单来说,把该隐藏的隐藏,该暴露的暴露,这就是封装性的设计思想。
程序设计高内聚,低耦合:
高内聚:类的内部操作细节自己完成,不允许外部干涉。
低耦合:仅对外暴露少量的方法用于使用。
封装性的体现:
例如对属性进行私有化(private),提供公共的方法来获取与设置属性,不随意让人直接获取并设置值。
想要获取并设置值只能通过特定的set/get方法来进行。
设计模式包含单例模式(获取实例不能通过new,而是通过一个公共方法获得),特定场景进行使用。
问题:也看到过某些setter没有做任何事情,仅仅只是将值设给变量而已,这样不是只会增加执行的负担吗?
答:这对getter也是一样的,好处是你事后可以改变想法却不会需要改变其他部分的程序。假设说所有人都使用你的类以及共有变量,万一有一天你发现这个变量需要检查,那不是所有人都要跟着改成调用setter嘛,这就是封装的优点之处,直接进行存取变量的效率的好处比不上其封装的好处。考虑到之后改变心意,将程序改的更安全、更快、更好。
权限修饰符
封装性的体现,需要权限修饰符来配合使用
权限修饰符:private、public、default(缺省)、protected
缺省:没有权限修饰符的属性默认为缺省
修饰类与类的内部结构:属性、方法、构造器、内部类
注意:四个权限修饰符对于这几个结构都能够使用。
修饰类的修饰符:public、default(缺省)
public class Main{ public static void main(String[] args){\ } } //默认是default(缺省) class Person{ }
四种权限访问修饰符,如图:
二、特征二:继承
1、继承介绍
继承性(inheritance)
继承好处介绍:减少代码冗余、提高复用性;便于功能扩展;多态的提前。
格式:class A extends B{}
A:又叫子类、派生类、subclass。
B:又叫父类、超类、基类、superclass。
extends:表示延伸、扩展含义。
继承说明:
子类A继承父类B之后,子类A能够获取父类B声明的所有属性与方法,并且能够声明自己持有的属性与方法,实现功能的扩展。
父类B中声明为private的私有属性及方法,子类A继承B仍然认为获取了父类中私有的结构,因为封装性的原因,在子类A中无法直接调用父类的结构而已。
继承规则规定:
一个类可以被多个子类继承。
Java具有单继承性,不允许多继承,一个类只能有一个父类。
子类直接继承的父类:直接父类。间接继承的父类:间接父类。
子类继承父类之后,不仅仅获取直接父类的属性与方法,还获取了所有间接父类的属性及方法。
2、方法的重写
重写(override/overwrite)
方法重写:在子类中根据对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。
规则:
子类对继承父类被重写的方法有相同方法名称、参数列表。
子类重写的方法返回值类型不能大于父类被重写方法的返回值类型。
若父类被重写方法返回值为void,子类重写方法返回值只能也是void
…返回值为A类型,…返回值可以是A类或A类的子类
…返回值为基本类型,…返回值类型必须是相同的
子类重写方法使用的访问权限不小于父类被重写方法的访问权限。
子类不能重写父类中声明为private权限方法。
子类方法抛出异常不能大于父类被重写方法的异常。
注意:子父类中同名同参数的方法要在声明为非static的情况下才能重写。父类的方法若是static,子类与父类的同名同参数的方法必须也是static,但这并不是重写(static属于类,子类无法覆盖父类static方法,是独有的)。
3、super关键字
super:在子类中进行使用,具体有三个用处
情况一:在子父类中若有相同名字的属性,在内存中实际上是有两个不同的内存地址(方法可覆盖,属性不可覆盖),这个时候就需要通过使用this.子类属性与super.父类属性来进行区分属性。
情况二:使用super.属性,若是直接父类没有,会自动往间接父类上去找。
情况三:super调用构造器:
在子类中使用super(参数列表)来调用父类的构造函数
super(参数列表)必须声明在子类构造器的首行
构造器中super(参数列表)与this(参数列表)不能够同时出现
子类如果继承父类的话,在子类的构造器中的首行若是你自己不定义super(参数列表)与this(参数列表),会默认使用父类中空的构造器,也就是super(),若是此时父类恰好没有空参构造器就会报编译错误。
4、对象实例化过程
结果上来看(继承性):子类继承父类之后,会自动获取父类的属性或方法,在创建子类对象的过程中,堆空间不仅创建子类的内存空间,也会加载其直接父类、间接父类也就是所有父类声明的属性。
构建一个对象时,jvm会在堆中给对象分配空间,这些空间用来存储当前对象实例属性以及其父类的实例属性。
过程来看:在通过子类构造器创建对象时,一定会直接或间接的调用父类的构造器,进而调用父类的父类构造器,一直到java.lang.Object类中的空参构造器为止。正因为加载过所有父类的结构,才可以看到父类中结构,子类对象才会考虑进行调用。
不断的调用父类构造器也是进行栈的一个过程,栈顶的构造器执行完后才不断往下进行执行。
针对于创建的对象:虽然创建子类对象调用了父类的构造器,但是自始至终只创建过一个对象(实例化),也就是new的子类对象。(在对应堆中的内存空间中会加载父类的结构)
对于Snowboard只有Object父类时创建实例,堆中如下图:
三、特征三:多态性
介绍多态
多态介绍,编译、运行时类型两个概念,何时出现多态
多态性:面向对象中最重要的概念。具体就是父类的引用指向子类的对象,可以直接应用在抽象类和接口上。
引用变量的两个类型:编译时类型与运行时类型。简而言之编译时看左,运行时看右。
编译时类型:由声明该变量时使用的类型决定。
运行时类型:由实际赋给该变量的对象决定。
当编译时类型与运行时类型不一致时,就出现了对象的多态性(Polymorphism)。在多态情况下,看左边就是父类的引用(此时父类中不具备子类特有的方法),看右边是子类的对象(实际运行的是子类重写父类的方法)。
多态与非多态情况下不同引用的区别:引用《head first java》
非多态情况下创建自己本身实例会有所有的掌控权;多态情况下只能掌握其左边引用的声明类。
虚拟方法与动态绑定
虚拟方法调用(Virtual Method Invocation)
虚拟方法:若子类定义了与父类同名同参数的方法,多态情况下,将父类中的方法都成为虚拟方法。父类根据赋给它的不同子类对象,会动态的调用属于子类的该重写方法,这样的方法调用在编译器无法确定,在运行时才能确定。
动态绑定:编译时使用的是父类引用,而方法的调用是在运行时确定的,并且调用的是子类重写的方法。
public class Main { public static void main(String[] args){ Person stu = new SmallStu(); stu.introduce();//i am SmallStu的方法 } } class Person{ public void introduce(){ System.out.println("i am Person的方法"); } } class SmallStu extends Person{ @Override public void introduce() { System.out.println("i am SmallStu的方法"); } }
编译时stu是指向父类引用,运行时会看右边的子类对象,当调用其方法时编译期间只会去找父类引用的方法,运行时会调用子类重写的方法,所以结果是调用的子类的方法。
行为描述及场景使用
对于多态一定是运行时行为。多态时在编译期时只能调用引用父类的方法,若是调用的方法是对应子类对象重写的,会在运行期间确定并调用。
场景示例如下:
设置了两个子类分别继承了父类并重写方法
//父类Person class Person{ public void introduce(){ System.out.println("i am Person"); } } //子类 class ChangLu extends Person{ @Override public void introduce() { System.out.println("i am ChangLu"); } } //子类 class LinEr extends Person{ @Override public void introduce() { System.out.println("i am LinEr"); } }
public class Main { public static void main(String[] args){ introduceSelf(new Person()); introduceSelf(new ChangLu()); introduceSelf(new LinEr()); } //这个方法主要调用其参数中的方法 public static void introduceSelf(Person person){ person.introduce(); } }
introduceSelf方法会在调用时输出传入参数的不同身份,这里可以使用多态,将父类引用变量作为参数,那么不同的子类对象传入进行来时都会在运行期间调用其自己本身的方法。
多态还有其他使用场景,例如数据库连接方法中,定义参数为一个连接类父类,那么传入进行的对象若是不同的继承其连接类父类的子类,就能够根据不同数据库进行连接操作。
重载与重写(早、晚绑定)
这里描述的是重载与重写关于早绑定与晚绑定
重载:就是一个类中可以存在多个同名方法,只要其参数不同即可形成重载。编译器对具有相同方法名称但不同参数列表的方法名称会进行修饰。在编译器眼中我们看起来相同名称的重载方法就成了不同的方法,其调用地址在编译时期就进行绑定。
重载可以包含父类和子类的,也就是说子类可以重载父类的同名不同参数的方法。
编译期间即可确定方法称为早绑定或静态绑定。
重写:针对于父子类继承关系的类才会有重写方法的机会,重写的是继承父类同名同参数的方法,若是定义引用变量是多态情况下,编译期调用的是父类方法,而真正运行期会去调用其重写的方法。
运行期间确定调用方法称为晚绑定或动态绑定。
主要不是晚绑定就不是多态!!!
Instanceof(包含向下转型介绍)
前言描述
例如:A是B的父类,A b = new B();
进行多态后,实际上内存中还是加载了B的属性,在使用多态时只对方法有效,对属性无效,并且调用的也只能是父类的属性。
举个简单的例子吧,A中有B相同的属性及方法,然后进行了上面的多态,你输出该属性以及调用方法(子父类都相同给的),最终结果:属性是父类的,方法是子类重写的。
由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性与方法,子类特有的属性和方法不能调用。
如何在多态后调用子类特有的属性及方法呢?instanceof出现
多态:A a = new B();
可以通过向下转型或称作多态的方式:B b = (B)a;
注意:需要有子父类关系的情况下才能够进行向下转型。对于不是子父类关系强转编译时就会报错。若是有子父类关系的将原本父类转为子类的编译时不会报错,运行时会报异常(java.lang.ClassCastException)。
//情况一:将两个没有子父类关系的进行强转,编译时就会有异常 Dog d = new Dog(); Person p = (Person)d; //情况二:运行时异常,父类向下转子类(非多态) //Person父类,boy是继承Person的子类 Person p = new Person(); Boy b = (Boy)p;//会报转换异常java.lang.ClassCastException
问题出来了我们就需要解决,为了避免向下转型出现异常,通过进行a instanceof B的判断返回true或false来判定是否可以进行向下转型。
语法:例如a instanceof B,a部分必须是实例,B才是类,判断a是否是B的实例。
实际应用:方法参数是多态情况下进行判断向下转型获取值
//为了测试将属性都设置为了public,用于进行向下转型打印属性 class Person{ public String name = "person"; } class Boy extends Person{ public String name = "Boy"; } class Girl extends Person{ public String name = "Girl"; } public class Main { public static void main(String[] args){ introduceSelf(new Person()); introduceSelf(new Boy()); introduceSelf(new Girl()); } //打印每个人的各自名称() public static void introduceSelf(Person person){ //判断是否是Boy的实例,切勿出现这种情况:Person person = new Girl(); Boy boy = (Girl)person; if(person instanceof Boy){ Boy boy = (Boy)person; System.out.println(boy.name); }//判断是否是Boy的实例,切勿出现这种情况:Person person = new Boy(); Girl girl = (Girl)person; else if(person instanceof Girl){ Girl girl = (Girl)person; System.out.println(girl.name); }else{ System.out.println(person.name); } } }
这边再添加一个案例,对于向下转型的对象的内存地址依旧是转型前的内存地址。
//Boy是Person的子类 Person person = new Boy(); System.out.println(person);//Boy@677327b6 Boy boy = (Boy) person; System.out.println(boy);//Boy@677327b6