#说明
java面向对象的主要特点是三个。封装、继承、多态,本内容主要讲解比较重要的知识点。
封装
封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
具体来说,在java中使用private实现变量或者方法的私有化。
不使用封装
首先我们在不使用封装的情况下创建一个类并初始化一个对象
1 | //创建类 |
2 | public class Person { |
3 | public String name = "小明"; |
4 | public int age = 10; |
5 | } |
6 | //创建对象 |
7 | Person person = new Person(); |
此时直接使用person.name就能访问到person的姓名,这也太方便了吧,但是需要知道的是,使用public修饰的变量,在java任意位置的其他类都能访问到,比如,我建立了一个新的类,专门用来该你的名字
1 | //老王来了 |
2 | public class Laowang { |
3 | void change_name(Person person){ |
4 | person.name = "小王"; |
5 | System.out.println("你儿子归我了"); |
6 | } |
7 | } |
8 | //老王偷你儿子 |
9 | Laowang laowang = new Laowang(); |
10 | laowang.change_name(person); |
然后你会发现你儿子的名字被改了。这个时候你就该想了,怎么样能让儿子不被偷呢?
使用封装
private就来了,private就是让你把自己的变量或者方法藏起来,更绝的是,你自己都不能直接访问自己的变量,那咋办呢,这个时候set和get方法就来了
1 | public class Person { |
2 | private String name = "小明"; |
3 | private int age = 10; |
4 | |
5 | public String getName() { |
6 | return name; |
7 | } |
8 | |
9 | public void setName(String name) { |
10 | this.name = name; |
11 | } |
12 | |
13 | public int getAge() { |
14 | return age; |
15 | } |
16 | |
17 | public void setAge(int age) { |
18 | this.age = age; |
19 | } |
然后我们就能通过get和set来访问和修改了,而老王他就改不了你的名字了。但是这个时候你会发现,别人还是可以通过set和get来访问和修改的,比如我们将getName的方法修改一下,再新建一个对象访问
1 | public String getName(Person p) { |
2 | return p.name; |
3 | } |
4 | // |
5 | Person person = new Person(); |
6 | Person person1 = new Person(); |
7 | System.out.println(person1.getName(person)); |
这样还是能通过person1访问person的name变量,那set也能改了,也就是说,老王又可以改名字了。那咋办呢,那我们就在get方法里面加个判断吧
1 | public String getName(Person p) { |
2 | if(this == p){ |
3 | return p.name; |
4 | } |
5 | else{ |
6 | System.out.println("你看个锤子看"); |
7 | } |
8 | return null; |
9 | } |
然后你就发现其他对象就看不了名字了。
总结
封装简单来看就是用private对类的成员变量和函数进行封装,然后再用get和set提供接口,同时可以使用this关键字进行更深层的判断。
继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为
具体实现时,在创建子类的时候,使用extends继承父类
为什么使用继承
这个问题解释起来其实挺简单的,举个简单的例子就是比如狗和猫都有嘴巴,眼睛,都会吃,走,但是我们如果在每个类里面都把这些变量和方法都写一遍,就会使代码产生产生冗余。比较好的方法就是创建一个动物类,包含啦嘴巴和眼睛,都会走,吃,而让猫,狗类继承动物类,再在各自内部实现各自独有的变量和方法。
1 | //动物类 |
2 | public class Animal { |
3 | protected String eye; |
4 | protected String mouse; |
5 | |
6 | Animal(){ |
7 | System.out.println("Animal create"); |
8 | } |
9 | protected void eat(){ |
10 | System.out.println("animal eat"); |
11 | } |
12 | protected void run(){ |
13 | System.out.println("animal run"); |
14 | } |
15 | } |
16 | //猫类 |
17 | public class Cat extends Animal { |
18 | |
19 | Cat(){ |
20 | System.out.println("cat create"); |
21 | } |
22 | |
23 | protected void run(){ |
24 | System.out.println("cat run"); |
25 | } |
26 | //添加猫抓老鼠的功能 |
27 | private void get_moues(){ |
28 | System.out.println("cat cna get moues"); |
29 | } |
30 | } |
31 | |
32 | //狗类 |
33 | public class Dog extends Animal { |
34 | |
35 | Dog(){ |
36 | System.out.println("dog create"); |
37 | } |
38 | |
39 | protected void run(){ |
40 | System.out.println("dog run"); |
41 | } |
42 | //添加狗看门的功能 |
43 | private void watch_door(){ |
44 | System.out.println("dog can watch door"); |
45 | } |
46 | } |
上面的例子中,猫和狗就不用再定义眼睛和嘴巴了,因为在继承的时候,相当于隐式的将这些变量在自己类中定义了。
构造函数和方法的重写
构造函数
子类是无法继承父类的构造函数的,也就是说在Dog类里面不能定义Animal()这个构造函数,但是在创建子类的时候可以调用。
如果父类的构造器带有参数的,则必须在子类的构造器中显式地通过super关键字调用父类的构造器。
1 | //父类添加带参的构造函数 |
2 | Animal(String eye, String mouse){ |
3 | this.eye = eye; |
4 | this.mouse = mouse; |
5 | } |
6 | |
7 | //子类需要使用super调用 |
8 | Cat(String eye, String mouse) { |
9 | super(eye, mouse); |
10 | } |
如果父类有无参构造器,则在子类的构造器中用super调用父类构造器不是必须的,如果没有使用super关键字,系统会自动调用父类的无参构造器。
子类在使用构造函数创建对象的时候都会先调用父类的构造对象,然后再调用自己的构造函数。
重载和重写
一般将重写,都会跟重载对应起来将。
重写又叫覆盖,发生在继承关系下的子类中。子类可以用自己的方式实现父类的方法,重写不能改变参数列表,也不能缩小方法的访问权限,如果父类方法抛出异常,子类抛出的异常不能比父类的异常“大”,也不能抛出新的异常。
上面的例子中,Animal内部有eat()方法,而Cat内部可以通过重写来改变eat()的内部实现,如果子类重写了父类的方法,那么在调用的时候就不会和构造函数一样再去父类调用一遍了。
需要注意的是,如果父类定义了一个private的方法,那么在子类中重新编写这个方法,都算不上重写,因为private修饰的方法是不能被子类继承的,重新编写这个函数只能算是子类自己重新构造了一个新的函数。
1 | //父类的private函数 |
2 | private void eat(){ |
3 | System.out.println("animal eat"); |
4 | } |
5 | |
6 | //子类用overide修饰会报错的 |
7 |
|
8 | public void eat(){ |
9 | |
10 | } |
而重载和继承没有任何关系(当然,继承之间也存在重载,也就是说,继承可以重载,但是重载不一定继承),它发生在类本身。重载方法的特点是方法名相同而参数列表不同。
1 | private void eat(){ |
2 | System.out.println("animal eat"); |
3 | } |
4 | private void eat(int a,int b){ |
5 | System.out.println("animal eat1"); |
6 | } |
7 | private void eat(int a,int b,double c){ |
8 | System.out.println("animal eat2"); |
9 | } |
只需要记住,被重载的方法必须改变参数列表(参数个数或类型或顺序不一样)
多态
分类
多态,简而言之就是同一个行为具有多个不同表现形式或形态的能力。
在java中,多态主要包括重写式多态和重载柿多态,也就是在继承中已经讲过的两个类别。
- 重载式多态,也叫编译时多态。也就是说这种多态在编译时已经确定好了。方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。
- 重写式多态,也叫运行时多态。这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。
多态的条件
主要考虑运行时的多态,主要包含以下条件
- 继承。在多态中必须存在有继承关系的子类和父类。
- 重写。子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型。在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
继承与重写已经基本介绍过了,下面主要讲向上转型
向上转型
子类引用的对象转换为父类类型称为向上转型。通俗地说就是将子类对象转为父类对象。此处父类对象可以是接口。
1 | Animal a = new Cat(); |
这就是向上转型,将子类创建的对象转化为父类对象,此时a调用的方法是子类Cat中的方法。
需要注意的是
- 向上转型时,子类单独定义的方法会丢失。比如在Cat类中定义父类没有的方法,当Animal引用指向Cat类实例时是访问不到除了继承外的其他方法。
- 子类引用不能指向父类对象。
Cat c = (Cat)new Animal()
这样是不行的。
向上转型可以使代码变得简洁
多态中的注意事项
在向上转型中,往往会涉及到函数的调用,由于父类和子类都定义了相同的函数,不知道应该调用哪一个。看下面这句话:
当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。
什么意思呢,比如我们有
1 | public class Animal { |
2 | protected void test(Animal a){ |
3 | System.out.println("A_a"); |
4 | } |
5 | protected void test(Cat c){ |
6 | System.out.println("A_c"); |
7 | } |
8 | } |
1 | public class Cat extends Animal { |
2 | protected void test(Animal a){ |
3 | System.out.println("C_a"); |
4 | } |
5 | protected void test(Cat c){ |
6 | System.out.println("C_c"); |
7 | } |
8 | |
9 | } |
1 | Animal a = new Cat(); |
2 | Animal a1 = new Animal(); |
3 | a.test(a1); |
a决定了可以调用的方法,也就是可以使用protected void test(Animal a)和protected void test(Cat c),而Cat决定了调用哪个类中的方法,也就是使用Cat类中的test方法。
继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)
当子类无法在自身找到可调用的方法的时候,会根据上述的继承链依次调用。