封装
简介
封装就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(方法),才能对数据进行操作。
Java核心技术的解释 —— 将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式(比如调用Math.random,不必了解如何具体实现)。对象中的数据称为实例字段(比如String name等),操作数据的过程称为方法。实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。
好处
- 隐藏实现细节,使用者只需要调用即可。类似的,电视机也是一种封装,用户想要开机等操作,只需要在遥控器上按一下就可以了,不用知道电视机内部进行的复杂操作。
2. 可以对要进行赋值的数据进行检验,保证安全合理。int age = 12000; //这就是没有封装的坏处,直接就对一个变量赋值,没有对赋值的数据进行检验
封装步骤
1. 将属性进行私有化 private,这样就不能直接修改属性了。
2. 提供一个公共的(public) set方法,用于检验数据并给属性赋值。
3. 提供一个公共的 get方法,用于获取属性的值。public void setXxx(类型 参数名){
//加入数据验证的业务逻辑
属性 = 参数名;
}
public 数据类型 getXxx(){
return Xxx;
}
封装与构造器
为了防止调用构造器绕过set函数,我们可以把set函数写在构造器中。public Person(String name,int age,double salary){
this.setName(name);
this.setAge(age);
this.setSalary(salary);
} //在构造器中写入封装方法
继承
基本介绍
继承可以解决代码复用,让我们的编程更加靠近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可。示意图
基本语法
class 子类 extends 父类{}
- 子类会自动拥有父类定义的属性和方法(当然受访问修饰符的限制)。
2. 父类又叫超类,基类。
3. 子类又叫派生类。
4. 假设A继承B,B继承C,那么C也算A的父类。注意事项
- 子类继承了父类所有的属性和方法,非私有的属性和方法可以在子类直接访问,但是私有属性和方法不能在子类直接访问,要通过父类提供公共的方法去访问。
2. 子类必须调用父类的构造器,完成父类的初始化。
3. 当创造子类对象时,不管使用子类哪个构造器,默认情况下都会去调用父类的无参构造器,相当于子类的构造器有一个默认语句 super()。 ```java public class Pupil extends Person{
double score;
Pupil(){
}System.out.println("调用pupil构造器");
public static void main(String[] args) {
} }Pupil pupil = new Pupil(); // 调用子类Pupil的构造器
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23175776/1641723937499-f06fc310-ee4e-4f25-9402-6468858ba3a2.png#clientId=u5b1043af-a010-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u70011396&margin=%5Bobject%20Object%5D&name=image.png&originHeight=195&originWidth=940&originalType=url&ratio=1&rotation=0&showTitle=false&size=22525&status=done&style=none&taskId=ufc9af0b1-14ff-458a-abea-f8e5852ee21&title=)<br />**发现首先调用的是父类的构造器,然后再调用子类的构造器。**<br />4. **如果父类没有提供无参构造器,则必须在子类的构造器中用super指定使用父类的某个构造器完成对父类的初始化工作,否则编译不会通过**。
```java
Person(int age,String name){ //父类的构造函数,有参数
this.age = age;
this.name = name;
}
public class Pupil extends Person{
//继承的子类
double score;
Pupil(){
System.out.println("调用pupil构造器");
}
public static void main(String[] args) {
Pupil pupil = new Pupil(); //直接报错,因为没有调用父类的构造器
}
}
Pupil(){
super(10,"wang");
System.out.println("调用pupil构造器");
} //正确,用super调用父类构造器
- 如果希望指定去调用父类的某个构造器,则显式的调用一下:super(参数列表)。
6. super在使用时,必须放在构造器第一行( super() 调用构造器这种方法只能在构造器中使用)
7. super() 和 this() 都是调用构造器的方法,都必须放在构造器的第一行,因此这两个方法不能共存在一个构造器。Pupil(){
System.out.println("调用pupil构造器");
super(10,"wang"); //报错,super调用构造器必须放在第一行
}
8. Java中所有类都是Object类的子类(可以用ctrl + H查看)。
9. 调用子类构造器时,会向上一直调用父类的构造器,一直追溯到Object类。public static void main(String[] args) {
Pupil pupil = new Pupil(); //从上往下开始调用构造器
}
10. 子类最多只能继承一个父类(直接继承,删除了C++的多重继承),Java中是但继承机制。那么任何让A类同时继承B类和C类? A 继承 B,B 继承 C。
11. 不能滥用继承,子类和父类之间必须满足 “is - a”(也就是必须有共同点)的逻辑关系。Music extends Person //不合理
Cat extends Animal //合理
本质分析
分析当子类继承父类,创建子类对象时,内存中到底发生了什么。
要按照查找关系来返回信息:Son son = new Son();
System.out.println(son.name); //本类中有该信息,返回 "小头儿子"
System.out.println(son.age); //父类中有该信息,返回 39
System.out.println(son.hobby); //父类的父类有该信息,返回 "旅游"
1. 首先看子类是否有该属性,如果子类有这个属性,则返回信息(因为是在本类中,因此绝对可以访问,即使是private)。
2. 如果子类没有这个属性,就看父类有没有这个属性,如果父类有该属性,并且可以访问,就返回信息,如果该信息不可访问(private),那么就直接报错(can not access),不会再向上查找了。
3. 如果父类没有该信息,那么就按照 (2) 的规则,继续找上级,直到Object,如果到最顶端也没有查找到,则提示该方法不存在。
多态
对于解决某些问题,比如 假设需要创建一个方法给宠物喂食,那么用传统方法来说,给猫喂鱼,狗喂骨头等等,需要每一个都写一个方法,这样导致代码的复用性不高,而且不利于代码的维护,因此需要多态。
多态指 方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
方法的多态
重写和重载就体现了多态。
// 方法重载体现了多态——在同类中方法有多种形态
public int sum(int num1,int num2){
return num1 + num2;
}
public int sum(int num1,int num2,int num3){
return num1+num2+num3;
}
// 方法重写体现了多态——在不同类中方法也有多种形态
public int sum(int num1,int num2)
{
return num1 + num2;
} //父类方法
public int sum(int num1,int num2)
{
return num1 - num2;
} //子类方法
对象的多态
- 一个对象的编译类型和运行类型可以不一致。编译类型看定义时 =号 的左边,运行类型看 =号 的右边。
2. 编译类型在定义对象时就确定了,不能改变。Animal animal = new Dog(); // animal 编译类型是Animal,运行类型为 Dog
3. 运行类型是可以变化的。
4. 对象多态的前提:两个对象(类)之间存在继承关系。animal = new Cat(); // animal的运行类型变成了Cat,编译类型仍然是Animal
因为对象的多态,很多操作就简洁许多了。以上面的喂食例子,我们正常写的话应该是这样:
而如果用对象的多态方法来写,则是下面这样:当调用时,仍然是传入Animal类和Food类的子类,这时就用到了多态机制。public void feed(Cat cat, Fish fish){
...; //输出喂食信息
}
public void feed(Dog dog, Fish fish){
...; //输出喂食信息
}
...... //所有动物类与食物类都要写一遍,写在Master类中
5. 多态参数:方法定义的形参类型为父类类型,实参类型允许为子类类型。public void feed(Animal animal, Food food){
...;
} // 只需在Master类中写一个即可
向上转型
1. 本质:父类的引用指向了子类的对象。
2. 语法:父类类型 引用名 = new 子类类型();
3. 特点:
1)编译类型看左边,运行类型看右边。
2)可以调用父类中的所有成员(当然前提是遵守访问权限)。
3)不能调用子类中特有属性和方法(因为在编译阶段,能调用哪些成员,是由编译类型来决定的。如果子类重写了父类的方法,那么由于动态绑定机制,调用父类的成员和方法会先从子类找,因此子类重写的方法是可以被调用的。但是如果子类重写了父类的属性,那么调用看的还是编译类型,因为属性没有动态绑定机制。 总结:调用重名属性看编译类型,调用重名方法看运行类型。 ```java public class Car { public int age; public void say(){
} }System.out.println("这是一个car类");
class BMW extends Car{ public int age = 10; //同名属性 public void say(){ //同名方法 System.out.println(“这是一个BMW类”); } public void hi(){ //独特方法 System.out.println(“hello”); } public static void main(String[] args) { Car car = new BMW(); Car car1 = new Car(); BMW bmw = new BMW(); System.out.println(car.age); //0 编译类型为Car System.out.println(car1.age); //0 System.out.println(bmw.age); //10 car.say(); //BMW类 运行类型为BMW car.hi(); //报错,编译类型是Car,不能调用BMW的独特方法 car1.say(); //Car类 bmw.say(); //BMW类 } }
```java
Animal animal = new Cat();
animal.catchMouse(); //错误,因为catchMouse是子类的特有方法,向上转型不能调用
向下转型
1. 语法:子类类型 引用名 = (子类类型) 父类引用。
Cat cat = (Cat)animal;
这里注意一点,向下转型后,cat的编译类型和运行类型都是Cat,cat和animal都指向Cat的对象(注意animal没有消失)。
2. 只能强转父类的引用,不能强转父类的对象。父类的对象是创建在堆区中的,这个是不能改变的(因为已经创建了),但是可以改变指向该对象的指针,向下转型就是让它指向一个子类的对象。
3. 要求父类的引用必须指向的是当前目标类型的对象。
Animal animal = new Cat();
Dog dog = (Dog)animal; //报错
Cat cat = (Cat)animal; //正确
就比如这个例子,父类的引用本来就指向Cat类的对象,因此向下转型只能使用Cat。按照错误语句的理解,让一只狗指向猫对象,那肯定是错误的。
4. 当向下转型后,可以调用子类类型中所有的成员(当然要符合访问范围)。
5. 下面是错误的向下转型写法,不能让没有引用的对象进行向下转型。
Cat cat = (Cat)(new Animal());
Cat cat = (Cat)new Animal();
// 这两种写法都是错误的,不能让没有引用的对象进行向下转型
// 编译器报告 cannot be cast to 错误
多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型,静态初始化可以直接写,动态初始化则需要new父类,然后对里面的元素单个向上转型。
Person[] a = new Student[3]; //不能这样写
// 第一种写法,静态初始化
Preson p = new Person();
Person t = new Teacher();
Person s = new Student();
Person[] persons = {p,t,s};
//第二种写法,动态初始化
Person[] a = new Person[3]; //new父类
a[0] = new Student();
这里需要介绍一个问题:如何调用子类特有的方法(很明显,光有向上转型是不能调用的,向上转型只能让子类加入到父类的数组中,因此需要用到向下转型)
public class Person {
private int age;
private String name;
}
public class Student extends Person{
public void Study(){
System.out.println("学生正在学习");
}
}
public class Test {
public static void main(String[] args) {
Person[] a = new Person[3];
a[0] = new Student(); //向上转型
a[0].Study();
//报错,因为Study是子类特有的方法,向上转型不能调用(调用方法由编译类型决定)
}
}
public class Test {
public static void main(String[] args) {
Person[] a = new Student[3];
a[0] = new Student();
Student stu = (Student)a[0]; //向下转型
stu.Study();// 语义相同:((Student) a[0]).Study();
}
}
房屋出租小项目的思路
Java的动态绑定机制
1. 当调用对象方法的时候(不管是直接调用还是方法里调用),该方法会和该对象的内存地址(运行类型)绑定,根据运行类型进行调用,如果没有该方法,就启用继承机制。
2. 当在方法中使用对象属性时,没有动态绑定机制,调用哪个类的方法,就用哪个类的属性,如果没有就启用继承机制。用一个例子解释清楚:
public class Computer {
public int i = 10;
public int sum(){
return getI() + 10;
}
public int sum1(){
return i + 10;
}
public int getI(){
return i;
}
}
public class NotePad extends Computer{
public int i = 200;
public int sum(){
return i + 100;
}
public int getI(){
return i;
}
public int sum1(){
return i + 200;
}
}
父类和子类的成员完全相同,只不过内容有变化,注意sum不是子类的特有方法,因此向上转型之后可以调用。
public static void main(String[] args) {
Computer a = new NotePad(); //向上转型,运行类型为 NotePad
System.out.println(a.sum()); //输出 300,用的子类的方法和子类的i
System.out.println(a.sum1()); //输出 400
System.out.println(a.i); //输出 10,为父类的i
}
可以发现,由于运行类型是NotePad,当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)绑定,因此调用的方法直接从子类开始找,由于当在方法中使用对象属性时,没有动态绑定机制,调用哪个类的方法,就用哪个类的属性。子类的sum方法用到了 i,根据这条定理,应该用子类的 i。但是注意一点,如果直接调用a的i,由于属性没有重写,调用属性要看编译类型,因此使用父类的 i。
然后把子类的sum方法和sum1方法全部删除:
public class NotePad extends Computer{
public int i = 200;
public int getI(){
return i;
}
}
public class Test {
public static void main(String[] args) {
Computer a = new NotePad();
System.out.println(a.sum()); //输出210,父类的sum方法调用的是子类的getI方法 System.out.println(a.sum1()); //输出20,用父类的i
}
}
当把子类的sum方法和sum1方法都删除后,调用这些方法就要启动继承机制了(因为子类没有这两个方法),在父类中寻找这两个方法。注意父类的sum方法调用了一个 getI 函数,在方法里调用的方法也和运行类型绑定,因此调用的是子类的getI方法,返回的是子类的i,因此结果为210,而父类的sum1方法调用了属性 i,由于属性没有动态绑定机制,因此调用自己的 i,结果为20。
再次强调,属性没有重写之说,调用哪个属性要看编译类型。
public class Computer {
String name = "我是一台电脑"; //在父类定义一个属性
}
public class PC extends Computer{
String name = "我是一台PC"; //在子类定义一个相同名字的属性
}
public class Test {
public static void main(String[] args) {
Computer c1 = new Computer(); //编译类型为Computer
Computer c2 = new PC(); //向下转型,编译类型为Computer
PC c3 = new PC(); //编译类型为PC
PC c4 = (PC)new Computer(); //向上转型,编译类型为PC
System.out.println(c1.name);
System.out.println(c2.name);
System.out.println(c3.name);
System.out.println(c4.name);
}
}
调用属性与编译类型保持一致。
单例设计模式
设计模式
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,免去我们自己再思考和摸索。
单例设计模式
采取一定方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
单例模式有两种方式:1. 饿汉式 2. 懒汉式
饿汉式
1. 构造器私有化——防止直接new出来一个对象。
2. 类的内部创建一个静态对象。
3. 向外暴露一个静态的公共方法,该方法返回一个静态对象。
public class GirlFriend {
private String name;
private static GirlFriend gf = new GirlFriend("小红"); //内部创建的对象
public static GirlFriend getInstance(){
return gf;
} //对外公开的获得对象方法
private GirlFriend(String name) {
this.name = name;
} //构造器私有化
}
public class Test {
public static void main(String[] args) {
GirlFriend gf1 = GirlFriend.getInstance();
GirlFriend gf2 = GirlFriend.getInstance(); //都返回 gf
if(gf1 == gf2){
System.out.println("同一个人"); //输出 同一个人
}
}
}
注:饿汉式在内部就创建了对象,如果不使用的话会造成内存空间的浪费。
懒汉式
- 仍然构造器私有化。
2. 定义一个static静态属性对象(注意不new)。
3. 提供一个public的static方法,可以返回一个static对象。
4. 当用户第一次使用getInstance方法时,才会创建static对象,再次调用就返回那个静态对象。public class Person {
private static Person aa; //私有经典对象
private Person(){
System.out.println("构造器调用");
}
private static Person getInstance(){
if(aa == null){
aa = new Person(); //调用时才分配空间
}
return aa;
}
}
区别
1. 二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载之前就创建了对象实例,而懒汉式是在使用时才创建。
2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。
3. 饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
4. 在JavaSE标准类中,java.lang.Runtime就是经典的单例模式。
instanceof比较操作符
instanceof 是 比较操作符,用于判断对象的运行类型是否为XX类或XX类的子类型。
语法是 实例对象 instanceof 类。
public static void main(String[] args) { 、
Computer c1 = new Computer();//运行类型为Computer
Computer c2 = new PC(); //运行类型为PC
System.out.println(c1 instanceof Computer); //true
System.out.println(c1 instanceof PC); //false
System.out.println(c2 instanceof Computer); //true
System.out.println(c2 instanceof PC); //true
}