目录
JAVA面向对象程序三大特性:封装、继承、多态。在类和对象阶段,主要研究的就是封装特性。
在了解封装之前,先来了解一下访问限定符。
访问限定符
java中主要通过访问权限来实现封装 ,类可以将数据以及封装数据的方法结合在一起,而访问权限用来控制方法或者字段能否直接在类外边使用。
java提供了四种访问修饰符
封装
封装就是把抽象出的数据[属性]和对数据的操作[方法]封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作[方法],才能对数据进行操作。
封装的实现
- 将属性进行私有化private
- 提供一个公开的set方法,用于对属性判断并赋值
- 提供一个公开的get方法,用于获取属性的值
案例:
写一个设定工资的小程序,要求用户输入自己的姓名、年龄、工资信息。其中输入的名字要求为2~4个字,年龄为0~100之间,若想设置工资就必须输入身份验证。
public class Test{
public static void main(String[] args) {
SetSalary salary = new SetSalary();
salary.setName("鸡,鸡");
salary.setAge(20);
salary.setSalary(3000.00);
salary.print();
}
}
class SetSalary{
public String name;
private int age;
private Double salary;
public SetSalary(){
}
public SetSalary(String name,int age,Double salary){
this.name = name;
this.age = age;
this.salary = salary;
//在构造器中加一道防护,因为用户可能通过构造器绕开检测机制
this.setSalary(salary);
this.setAge(age);
this.setName(name);
}
public void setName(String name){
if(name.length()>=2&&name.length()<=4){
this.name = name;
}else{
System.out.println("你的名字不符合要求");
System.exit(0);
}
}
public void setAge(int age){
if(age>0&&age<100){
this.age = age;
}else{
System.out.println("你的年龄不符合要求");
System.exit(0);
}
}
public void setSalary(Double salary){
System.out.println("请输入你的银行卡后四位");
Scanner scan = new Scanner(System.in);
String code = scan.nextLine();
if(code.equals("1234")){
this.salary = salary;
System.out.println("输入成功");
}else{
System.out.println("输入不正确,不能设置");
System.exit(0);
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Double getSalary() {
return salary;
}
public void print(){
System.out.println("姓名:"+this.name+"\n"+"年龄:"+this.age+"\n"+"工资:"+this.salary);
}
}
封装的好处
- 可以隐藏实现细节
- 可以对数据进行验证,保证安全合理
继承
现在有三个人,张三、李四、王五,若我们要描述他们的特点就需要将他们的一些属性抽取出来,用一个类来描述一个人。比如张三的身高175cm、体重60kg、喜欢看电影、他还会游泳。李四身高180cm、体重58kg、喜欢弹吉他、会打篮球。王五身高170cm、体重70kg、喜欢动漫、会制作鬼畜视频。
从上面我们发现若要描述他们,我们就需要创建三个类分别对应,但是这样做很麻烦,而且代码效率执行起来比较低。我们发现他们都有一些共同的属性:身高、体重、爱好,唯一不同的就是特长。那么我们只需要将他们的共同属性抽取出来,单独创建一个类,让张三、李四、王五的类去继承这个类就可以了,这样写起来代码简洁,逻辑性强,而且复用性高。
继承的概念
将若干类中相同的属性、方法抽取出来创建一个父类,其他的类不用在单独写共同的属性、方法。而只需要继承父类即可,继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。
继承示意图
继承的基本语法
class 子类 extends 父类{
}
继承演示
import java.util.Scanner;
public class Test{
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("哈巴狗");
dog.setAge(2);
dog.eat();
dog.print();
System.out.println("==========================");
Cat cat = new Cat();
cat.setAge(3);
cat.setName("加菲猫");
cat.sleep();
cat.print();
}
}
class Animal{
String name;
int age;
public Animal(){
}
public Animal(String name,int age){
this.age = age;
this.name = name;
this.setAge(age);
this.setName(name);
}
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
public int getAge(){
return this.age;
}
public void setAge(int age){
this.age = age;
}
public void eat(){
System.out.println(name+"正在吃饭");
}
public void sleep(){
System.out.println(name+"正在睡觉");
}
public void print(){
System.out.println("名字:"+this.name+"\t"+"年龄:"+this.age);
}
}
class Dog extends Animal{
public void bark(){
System.out.println(name+"汪汪汪叫");
}
}
class Cat extends Animal{
public void mew(){
System.out.println(name+"喵喵叫");
}
}
从上面可以看到,我们并没有在Dog、Cat类里面写吃饭、睡觉和打印信息的方法,而却可以调用这些方法,这就是继承,子类调用父类的方法。
为了更好的了解继承,这里不得不提出一个关键字super
super关键字
super代表父类的引用,用于访问父类的属性、方法、构造器
基本语法:
1.访问父类的属性——super.属性名,但不能访问父类的private属性
2.访问父类的方法——super.方法名(参数列表),不能访问父类的private方法
3.访问父类的构造器——super(参数列表);注意只能放在构造器的第一句并且只能出现一句
下面分别演示三个基本语法
public class Test {
public static void main(String[] args) {
B b = new B();
}
}
class A{
public int n1 = 100;
protected int n2 = 121;
int n3 = 0;
private int n4 = 99;
public A(){
}
public A(String name){}
public A(String naem,int age){}
public void test1(){}
protected void test2(){}
void test3(){}
private void test4(){}
}
class B extends A{
//演示访问父类的属性,但不能访问父类的private属性
public void test(){
System.out.println(super.n1+" "+super.n2+" "+super.n3);//这里不能访问父类的n4属性,会报错
}
//演示访问父类的方法,不能访问父类的private方法
public void hi(){
super.test1();
super.test2();
super.test3();
//这里不能访问父类的test4方法,会报错
}
//演示访问父类的构造器,注意只能放在构造器的第一句并且只能出现一句
public B(){
// super();
// super("simth");
super("jack",20);
}
}
super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员 ,访问的时候遵循就近原则。
演示调用子类中的某个成员(属性、方法)
public class Test {
public static void main(String[] args) {
B b = new B();
b.bark();
}
}
class Base{
public void cal(){
System.out.println("Base类的cal()方法被调用");
}
}
class A extends Base{
public void eat(){
System.out.println("正在吃东西");
}
}
class B extends A{
public void bark(){
cal();
}
}
调用的规则就是:
下面的调用规则适合没有关键字和关键字为this的情况,若是super关键字则没有在本类找这个动作
1.先在本类找,如果有,则调用
2.本类如果没有,则在父类找(如果有,并且可以调用,则调用)
3.如果父类没有,则继续找父类的父类
若查找属性的过程中,找到了,但是不能访问,则报错。如果查找属性的过程中,没有找到,则提示属性不存在。
。
super和this的比较
区别点 | this | super | |
1 | 访问属性 | 访问本类中的属性,如果本类没有此属性则从父类中继续查找 | 从父类开始查找属性 |
2 | 调用方法 | 访问本类中的方法,如果本类没有此属性则从父类中继续查找 | 从父类开始查找方法 |
3 | 调用构造器 | 调用本类构造器,必须放在构造器的首行 | 调用父类的构造器,必须放在子类构造器的首行 |
4 | 特殊 | 表示当前对象 | 子类中访问父类对象 |
继续回到继承
继承的细节问题
1.子类必须调用父类的构造器,完成父类的初始化(在调用子类构造器中,隐藏了super关键字)
2.当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类中的构造器中用super去指定使用父类的哪个构造器完成对父类的初始化工作,否者,编译不会通过
3.如果希望指定去调用父类的某个构造器,则显示的调用一下:super(参数列表)
继承在内存中的情况
下面以一个案例来展开继承在内存中的情况
public class Test {
public static void main(String[] args) {
Son son = new Son();
}
}
class GrandPa{
String name = "大头爷爷";
String hobit = "旅游";
}
class Father extends GrandPa{
String name = "大头爸爸";
private int age = 20;
}
class Son extends Father{
String name = "大头儿子";
}
分析:
第一步:在方法区中依次加载Object类、Grandpa类、Father类、Son类的信息
第二步:分别对Grandpa类、Father类、Son类中信息初始化并分配内存,将字符串在常量池中的地址返回给堆空间
第三步:将堆中的地址返回给son变量
我们可以看出子类继承父类,当创建子类对象好后,在内存中建立的是一种查找关系。
查找关系:
1.首先看子类是否有该属性或方法,有并且可以访问,则返回信息。
2.若子类没有,就看父类有没有这个属性,如果父类有该属性,并且可以访问,就返回信息
3.若父类没有,则按照2的查找关系依次向上找
注意若其中找到了,但不能访问就会报错。
重写override
重写:简单来说就是子类有一个方法,和父类的某个方法的名称、返回类型、参数一样,那么就说子类的这个方法重写了或者覆盖了父类的方法。
案列演示重写
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
}
}
class Animal{
public void eat(){
System.out.println("动物吃东西");
}
}
class Cat extends Animal{
public void eat(){//这个方法和父类的eat方法名称、返回类型、参数都一样
System.out.println("小猫吃东西");
}
}
重写的注意事项
1.子类方法的形参列表、方法名称要和父类的形参列表、方法名称完全一样
2.子类方法的返回类型和父类方法的返回类型一样,或者是父类返回类型的子类,比如父类返回类型是Object,子类方法的返回类型是String
3.子类方法不能缩小父类方法的访问权限(public>procted>默认>private)
4.父类被static、private修饰的方法不能被重写
重写和重载的比较
名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
重载 | 本类 | 必须一样 | 类型,个数或者顺序至少有一个不同 | 无需求 | 无需求 |
重写 | 父子类 | 必须一样 | 相同 | 子类重写的方法,返回的类型和父类返回的类型一致,或者是其子类 | 子类方法不能缩小父类方法的访问范围 |
多态
通过一个案列来引出多态
编写一个程序,Master类中有一个feed(喂食)方法,可以完成主人给动物喂食的信息
package Poly;
class Master{
private String name;
public Master(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void feed(Cat cat,Fish fish){
System.out.println("主人"+name+"给"+cat.getName()+"吃"+fish.getName());
}
public void feed(Dog dog,Bond bond){
System.out.println("主人"+name+"给"+dog.getName()+"吃"+bond.getName());
}
}
public class Animal {
private String name;
public Animal(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Cat extends Animal{
public Cat(String name){
super(name);
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
}
class Food{
private String name;
public Food(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Bone extends Food{
public Bone(String name){
super(name);
}
}
class Fish extends Food{
public Fish(String name){
super(name);
}
}
public class Test {
public static void main(String[] args) {
Master simth = new Master("simth");
Dog dog = new Dog("哈巴狗");
Bone bone = new Bone("排骨");
simth.feed(dog,bone);
System.out.println("=================");
Cat cat = new Cat("大橘");
Fish fish = new Fish("鲫鱼");
simth.feed(cat,fish);
}
}
我们发现上面这样写也能解决问题,但是如果动物一多,而且吃得食物还不一样,比如再添加一个动物Pig,它要吃白菜。我们就又得单独为他创建feed方法,这样做就很麻烦。这里我们就不得不引入另外一种机制,多态。
多态:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会出现不同的状态。
多态的实现条件:
1.必须在继承体系下
2.子类必须要对父类中方法进行重写
3.通过父类的引用调用重写的方法
多态的具体体现
1).方法的多态
重写和重载体现多态
public class Override {
public static void main(String[] args) {
A a = new A();
//方法的重载体现多态
System.out.println(a.sum(1, 2));
System.out.println(a.sum(1, 2, 3));
B b = new B();
//方法的重写体现多态
a.say();
b.say();
}
}
class B{
public void say(){
System.out.println("B say()方法被调用");
}
}
class A extends B{
public int sum(int n1,int n2){
return n1+n2;
}
public int sum(int n1,int n2,int n3){
return n1+n2+n3;
}
public void say(){
System.out.println("A say() 方法被调用");
}
}
2).对象的多态
重要知识:
1.一个对象的编译类型和运行类型可以不一样
2.编译类型在定义对象时,就确定了,不能改变
3.运行类型是可以改变的
编译类型看定义时=号的左边,运行类型看=的右边
public class Override {
public static void main(String[] args) {
Animal animal = new Cat();
animal.cry();
System.out.println("====================");
animal = new Dog();
animal.cry();
}
}
class Animal{
public void cry(){
System.out.println("Animal cry() 动物在叫");
}
}
class Cat extends Animal{
public void cry(){
System.out.println("Cat cry() 猫在叫");
}
}
class Dog extends Animal{
public void cry(){
System.out.println("Dog cry() 狗在叫");
}
}
上面animal的编译类型是Animal,运行类型分别是Cat、Dog,当调用方法的时候,也是调用运行类型相关的方法,这里就体现出来多态,编译类型必须一样,但运行类型可以有很多。
通过多态现在就能解决主人喂食的问题了
只需要将喂食这个动作变成多态就行了
package Poly;
class Master{
private String name;
public Master(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void feed(Animal animal,Food food){
System.out.println("主人"+name+"给"+animal.getName()+"吃"+food.getName());
}
}
public class Animal {
private String name;
public Animal(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Cat extends Animal{
public Cat(String name){
super(name);
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
}
class Food{
private String name;
public Food(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Bone extends Food{
public Bone(String name){
super(name);
}
}
class Fish extends Food{
public Fish(String name){
super(name);
}
}
这样做我们即使现在再添加其他动物也能保证代码的复用性。
多态的注意事项
- 多态的前提是:两个对象(类)存在继承关系
多态的向上转型,本质:父类的引用指向了子类的对象
语法: 父类类型 引用名 = new 子类类型();
特点:可以调用父类中的所有成员,不能调用子类中的特有成员(因为在编译阶段能调用那些成员,是由编译器类型决定的)
最终运行效果看子类的具体实现(调用方法和前面讲的继承的调用规则是一样的)
public class Override {
public static void main(String[] args) {
Animal animal = new Cat();//向上转型
animal.eat();//这里输出猫吃鱼,说明查找还是先从子类开始
animal.run();
animal.show();
//animal.sleep();报错:'sleep()' 在 'Animal' 中具有 private 访问权限
}
}
class Animal{
String name = "动物";
int age = 10;
private void sleep(){
System.out.println("睡觉");
}
public void run(){
System.out.println("跑");
}
public void eat(){
System.out.println("吃");
}
public void show(){
System.out.println("你好");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
public void catchMouth(){
System.out.println("猫抓老鼠");
}
}
class Dog extends Animal{
}
多态的向下转型
语法:子类类型 引用名 = (子类类型) 父类引用
要求父类的引用必须指向的是当前目标类型的对象,当向下转型后,可以调用子类类型中所有的成员
public static void main(String[] args){
Object a = new Integer(5);//可以,这里发生了向上转型
String str = (String)a;//不可以,将指向Integer的父类引用,转成String发生类型转化异常
Integer b = (Integer)a;//可以,向下转型
}
JAVA中的动态绑定机制
- 当调用对象方法的时候,该方法会和该对象的运行类型绑定
- 当调用对象属性时,没有动态绑定机制,哪里声明哪里使用
public class Test {
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum());//结果为40
System.out.println(a.sum1());//结果为30
}
}
class A{
public int i = 10;
public int sum(){
return getI()+10;
}
public int sum1(){
return i+10;
}
public int getI(){
return i;
}
}
class B extends A{
public int i = 20;
public int sum(){
return i+20;
}
public int getI(){
return i;
}
public int sum1(){
return i+10;
}
}
解析a.sum:
调用方法时有动态绑定机制,a的运行类型是B,则调用a.sum方法先在子类中查找,找到了而且这个方法不是子类特有的(父类中也有)。子类方法中的i就是本类中的20,因为属性没有动态绑定机制,所以返回的结果就是40。
解析a.sum1:
分析过程和上面一样,返回的结果是30
public class Test {
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum());//结果为30
System.out.println(a.sum1());//结果为20
}
}
class A{
public int i = 10;
public int sum(){
return getI()+10;
}
public int sum1(){
return i+10;
}
public int getI(){
return i;
}
}
class B extends A{
public int i = 20;
public int getI(){
return i;
}
}
解析a.sum:
调用方法时有动态绑定机制,a的运行类型是B,则调用a.sum方法先在子类中查找,子类中没有,在去父类中查找,找到了。父类中a.sum方法返回值有getI方法,根据动态绑定机制,他就会去a的运行类型中查找,找到了,再根据属性没有动态绑定机制,所以i就是20。所以a.sum方法的返回值为30。
解析a.sum1:
调用方法时有动态绑定机制,a的运行类型是B,则调用a.sum方法先在子类中查找,子类中没有,在去父类中查找,找到了。再根据属性没有动态绑定机制,所以i就是10。所以a.sum1方法的返回值为20。