🌠作者:@TheMythWS.
🎇座右铭:不走心的努力都是在敷衍自己,让自己所做的选择,熠熠发光。
目录
📃三大特性:封装、继承、多态
🟧封装🚫
【1】生活案例:
ATM , 电线
【2】Java中封装的理解:
将某些东西进行隐藏,然后提供相应的方式进行获取。
类和对象阶段,主要研究的就是封装特性。何为封装呢?简单来说 就是套壳屏蔽细节。
比如:对于电脑这样一个复杂的设备,提供给用户的就只是:开关机、通过键盘输入,显示器,USB插孔等,让用 户来和计算机进行交互,完成日常事务。但实际上:电脑真正工作的却是CPU、显卡、内存等一些硬件元件。 对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户 只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳 子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互.
【3】封装的好处:
提高代码的安全性
【4】代码:通过一个属性感受封装:
public class Girl {//女孩
//属性:
private int age;
//读取年龄:
public int duquAge(){
return age;
}
//设置年龄:
public void shezhiAge(int age){
if(age >= 30 ){
this.age = 18;
}else{
this.age = age;
}
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//创建一个Girl类的对象:
Girl g = new Girl();
/*g.age = 33;
System.out.println(g.age);*/
//设置年龄:
g.shezhiAge(31);
//读取年龄:
System.out.println(g.duquAge());
}
}
上面的代码,对于属性age来说,我加了修饰符private,这样外界对它的访问就受到了限制,现在我还想加上其他的限制条件,但是在属性本身上没有办法再加了,所以我们通过定义方法来进行限制条件的添加。
以属性为案例:
进行封装:
(1)将属性私有化,被private修饰--》加入权限修饰符
一旦加入了权限修饰符,其他人就不可以随意的获取这个属性
(2)提供public修饰的方法让别人来访问/使用
(3)即使外界可以通过方法来访问属性了,但是也不能随意访问,因为咱们在方法中可以加入限制条件。
【5】实际开发中,方法一般会写成 setter,getter方法:
可以利用IDEA快捷键生成:alt+insert -->getter and setter:
public class Girl {//女孩
//属性:
private int age;
//读取年龄:
public int getAge(){
return age;
}
//设置年龄:
public void setAge(int age){
if(age >= 30 ){
this.age = 18;
}else{
this.age = age;
}
}
}
【6】加深练习:
public class Student {
//属性:
private int age;
private String name;
private String sex;
//加入对应的setter和getter方法:
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
if("男".equals(sex) || "女".equals(sex) ){//sex是男 或者 女
this.sex = sex;
}else{
this.sex = "男";
}
}
//加入构造器:
public Student(){
}
public Student(int age,String name,String sex){
this.age = age;
this.name = name;
//this.sex = sex;
/*this.*/setSex(sex);//在同一个类中,方法可以互相调用,this.可以省略不写。
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//创建一个Student对象:
Student s1 = new Student();
s1.setName("nana");
s1.setAge(19);
s1.setSex("女");
System.out.println(s1.getName()+"---"+s1.getAge()+"----"+s1.getSex());
Student s2 = new Student(18,"菲菲","asdfasdfsadf");
System.out.println(s2.getName()+"---"+s2.getAge()+"----"+s2.getSex());
}
}
🟦继承👶
【1】类是对对象的抽象:
举例:
荣耀20 ,小米 红米3,华为 p40 pro ---> 类:手机类
【2】继承是对类的抽象:
举例:
学生类:Student:
属性:姓名,年龄,身高,学生编号
方法:吃饭,睡觉,喊叫,学习
教师类:Teacher:
属性:姓名,年龄,身高,教师编号
方法:吃饭,睡觉,喊叫,教学
员工类:Emploee:
属性:姓名,年龄,身高,员工编号
方法:吃饭,睡觉,喊叫,工作
共同的东西:
人类:
属性:姓名,年龄,身高
方法:吃饭,睡觉,喊叫
学生类/教师类/员工类 继承 自 人类
以后定义代码:
先定义人类:
人类: ---》父类,基类,超类
属性:姓名,年龄,身高
方法:吃饭,睡觉,喊叫
再定义 : ---》子类,派生类
学生类:Student:
属性:学生编号
方法:学习
教师类:Teacher:
属性:教师编号
方法:教学
员工类:Emploee:
属性:员工编号
方法:工作
子类 继承自 父类
狗类:
属性:姓名,年龄,身高
方法:吃饭,睡觉,喊叫
我们的继承关系,是在合理的范围中进行的抽取 ,抽取出子类父类的关系:
上面的案例中:
学生类/教师类/员工类 继承 自 人类 ---》合理
学生类/教师类/员工类 继承 自 狗类 ---》不合理
区分:
学生是一个人
教师是一个人
员工是一个人 ---》合理
学生是一个狗 ---》不合理
总结:继承 就是 is - a 的关系
【3】代码层面的解释:
先写父类,再写子类:
父类:人类 Person
子类:学生类 Student
public class Person {
//属性:
private int age;
private String name;
private double height;
//提供setter getter方法:
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
//方法:
public void eat(){
System.out.println("可以吃饭。。。");
}
public void sleep(){
System.out.println("可以睡觉。。。");
}
}
public class Student extends Person {//子类Student 继承 父类Person
//属性:
private int sno;//学号
public int getSno() {
return sno;
}
public void setSno(int sno) {
this.sno = sno;
}
//方法:
public void study(){
System.out.println("学生可以学习");
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//创建子类Student的对象
Student s = new Student();
s.setSno(1001);
s.setAge(18);
s.setName("菲菲");
s.setHeight(180.4);
System.out.println("学生名字为:"+s.getName()+",学生的年纪:"+s.getAge());
//访问方法:
s.study();
s.eat();
s.sleep();
}
}
【4】继承的好处:提高代码的复用性
父类定义的内容,子类可以直接拿过来用就可以了,不用代码上反复重复定义了
❗❗❗需要注意的点:
父类private修饰的内容,子类实际上也继承,只是因为封装的特性阻碍了直接调用,但是提供了间接调用的方式,可以间接调用。
(也就是向外提供了方法的接口)
【5】总结:
(1)继承关系 :
父类/基类/超类
子类/派生类
子类继承父类一定在合理的范围进行继承的 子类 extends 父类
(2)继承的好处:
1.提高了代码的复用性,父类定义的内容,子类可以直接拿过来用就可以了,不用代码上反复重复定义了
2.便于代码的扩展
3.为了以后多态的使用。是多态的前提。
(3)父类private修饰的内容,子类也继承过来了。
(4)一个父类可以有多个子类。
(5)一个子类只能有一个直接父类。
但是可以间接的继承自其它类。
(6)继承具有传递性:
Student --》继承自 Person ---》继承自Object
Object类是所有类的根基父类。
所有的类都直接或者间接的继承自Object。
内存分析
权限修饰符
【1】private:权限:在当前类中可以访问
【2】default:缺省修饰符(什么都不加):权限:到同一个包下的其他类都可以访问
【3】protected:权限:最大到不同包下的子类
【4】public:在整个项目中都可以访问
private < default < protectd < public
总结:
属性,方法:修饰符:四种:private,缺省,protected,public
类:修饰符:两种:缺省,public
以后写代码
一般属性:用private修饰 ,方法:用public修饰
方法的重写
【1】重写:
发生在子类和父类中,当子类对父类提供的方法不满意的时候,要对父类的方法进行重写。
【2】重写有严格的格式要求:
子类的方法名字和父类必须一致,参数列表(个数,类型,顺序)也要和父类一致。
【3】代码:
public class Person {
public void eat(){
System.out.println("吃食物");
}
public void sleep(){
System.out.println("睡觉");
}
}
public class Student extends Person {
public void study(){
System.out.println("学习");
}
@override
public void eat(){
System.out.println("我喜欢吃小龙虾喝啤酒。。");
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//创建一个Student类的对象:
Student s = new Student();
s.eat();
}
}
【4】内存:
【5】重载和重写的区别:
重载:在同一个类中,当方法名相同,形参列表不同的时候 多个方法构成了重载
重写:在不同的类中,子类对父类提供的方法不满意的时候,要对父类的方法进行重写。
🙌注意:如果是继承关系,也属于同一个类,子类可以重载父类的方法。
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。典型代表:多态的应用场合:把父类当作方法的形参,传入的是具体的子类,是具体的子类去调用方法。
💨练习题:
(1)求立体图形的表面积和体积:
import java.util.Scanner;
class Rect {
double l;//长
double h;//宽
double z;//高
public Rect(double l, double h, double z) {
if (l > 0 && h > 0 && z > 0) {
this.l = l;
this.h = h;
this.z = z;
}
}
public double area() {//底面积
return l * h;
}
}
class Cubic extends Rect {
public Cubic(double l, double h, double z) {
super(l, h, z);
}
@Override
public double area() {
return 2 * super.area() + 2 * l * z + 2 * h * z;
}
public double v() {
return super.area() * z;
}
}
class Pyramid extends Rect {
public Pyramid(double l, double h, double z) {
super(l, h, z);
}
@Override
public double area() {
return ((l * Math.sqrt(4 * Math.pow(z, 2) + Math.pow(h, 2))) + (h * Math.sqrt(4 * Math.pow(z, 2) + Math.pow(l, 2)))) / 2 + l * h;
}
public double v() {
return super.area() * z * 1 / 3;
}
}
public class Main {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double l, h, z;
while (sc.hasNext()) {
l = sc.nextDouble();
h = sc.nextDouble();
z = sc.nextDouble();
Cubic cubic = new Cubic(l, h, z);
Pyramid pyramid = new Pyramid(l, h, z);
System.out.printf("%.2f %.2f %.2f %.2f\n", cubic.area(), cubic.v(), pyramid.area(), pyramid.v());
}
}
}
(2)客车、皮卡车、货车问题:
import java.util.Scanner;
class Vehicle {
//int id; //编号
int price; //单价
static int sum; //载过的总人数
static double weight;//总重量
static int totol; //总金额
void computeSum() {
}
void computeWeight() {
}
void computeTotal() {
}
}
class Bus extends Vehicle {//客车
int number; //人数
Bus(int number, int price) {
this.number = number;
this.price = price;
}
void computeSum() {
sum += number;
}
void computeTotal() {
totol += price;
}
}
class Pickup extends Vehicle {//皮卡
int number;
double load;
Pickup(int number, double load, int price) {
this.number = number;
this.load = load;
this.price = price;
}
void computeSum() {
sum += number;
}
void computeWeight() {
weight += load;
}
void computeTotal() {
totol += price;
}
}
class Truck extends Vehicle {//货车
double load; //载重
Truck(double load, int price) {
this.load = load;
this.price = price;
}
void computeWeight() {
weight += load;
}
void computeTotal() {
totol += price;
}
}
class Main {
public static void main(String[] args) {
int number[] = {0, 5, 5, 5, 51, 55, 5, 5, 0, 0, 0,};
double load[] = {0, 0, 0, 0, 0, 0, 0.45, 2.0, 3, 25, 35};
int price[] = {0, 800, 400, 800, 1300, 1500, 500, 450, 200, 1500, 2000};
Scanner s = new Scanner(System.in);
int num = s.nextInt();
for (int i = 0; i < num; i++) {
int id = s.nextInt(); //租车编号
int day = s.nextInt(); //租车天数
if (id < 6) {//客车
Bus b = new Bus(number[id] * day, price[id] * day);
b.computeSum();
b.computeTotal();
} else if (id == 6 || id == 7) { //皮卡车
Pickup p = new Pickup(number[id] * day, load[id] * day, price[id] * day);
p.computeSum();
p.computeWeight();
p.computeTotal();
} else { //货车
Truck t = new Truck(load[id] * day, price[id] * day);
t.computeWeight();
t.computeTotal();
}
}
System.out.print(Bus.sum + " ");
System.out.printf("%.2f ", Bus.weight);
System.out.println(Bus.totol);
}
}
super
【1】super:指的是: 父类的
【2】super可以修饰属性,可以修饰方法;
在子类的方法中,可以通过 super.属性 super.方法 的方式,显示的去调用父类提供的属性,方法。在通常情况下,super.可以省略不写:
在特殊情况下,当子类和父类的属性重名时,你要想使用父类的属性,必须加上修饰符super.,只能通过super.属性来调用
在特殊情况下,当子类和父类的方法重名时,你要想使用父类的方法,必须加上修饰符super.,只能通过super.方法来调用
在这种情况下,super.就不可以省略不写。
【3】super修饰构造器:
其实我们平时写的构造器的第一行都有:super() -->作用:调用父类的空构造器,只是我们一般都省略不写
(所有构造器的第一行默认情况下都有super(),但是一旦你的构造器中显示的使用super调用了父类构造器,那么这个super()就不会给你默认分配了。如果构造器中没有显示的调用父类构造器的话,那么第一行都有super(),可以省略不写)
在继承关系中,在构造子类的时候,必须要先帮助父类构造,也就是先调用父类的构造器。
情况(1):如果父类没有重载构造器,也就是说系统会为父类自动分配一个无参构造器,那么在子类的构造器中,第一行默认有super();(可以省略不写),会调用父类的空构造器.
情况(2):如果父类重载了构造器,也就是说系统不会为父类再自动分配无参构造器,那么在子类的构造器的第一行必须是父类重载过的构造器,例如:super(age, name);
针对以上情况,建议在父类中加入无参构造器,以防子类在调用super();时候报错。
调用super构造器的过程图super(age,name)
在构造器中,super调用父类构造器和this调用子类构造器只能存在一个,两者不能共存:
因为super修饰构造器要放在第一行,this修饰构造器也要放在第一行:
改正二选一即可:
【4】以后写代码构造器的生成可以直接使用IDEA提供的快捷键: alt+insert
继承条件下构造方法的执行过程
class Person {
int age;
String name;
public Person(int age, String name) {
super();
this.age = age;
this.name = name;
}
public Person() {
}
}
class Student extends Person {
double height ;
public Student() {
}
public Student(int age, String name, double height) {
super(age, name);
this.height = height;
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
Student s = new Student(19,"feifei",160.8);
}
}
交换两个数的值
class MyValue {
private int val;
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
}
public class Test {
public static void swap(MyValue myV1, MyValue myV2) {
int tmp = myV1.getVal();
//myV1.val = myV2.val;
myV1.setVal(myV2.getVal());
//myV2.val = tmp;
myV2.setVal(tmp);
}
public static void main(String[] args) {
MyValue myValue1 = new MyValue();
myValue1.setVal(10);
MyValue myValue2 = new MyValue();
myValue2.setVal(20);
swap(myValue1, myValue2);
System.out.println(myValue1.getVal());
System.out.println(myValue2.getVal());
}
/*public static void swap(MyValue myV1,MyValue myV2) {
int tmp = myV1.val;
myV1.val = myV2.val;
myV2.val = tmp;
}
public static void main(String[] args) {
MyValue myValue1 = new MyValue();
myValue1.val = 10;
MyValue myValue2 = new MyValue();
myValue2.val = 20;
swap(myValue1,myValue2);
System.out.println(myValue1.val);
System.out.println(myValue2.val);
}*/
}
Object类
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。
默认会继承Object父 类。即所有类的对象都可以使用Object的引用进行接收。
所有类都直接或间接的继承自Object类,Object类是所有Java类的根基类。
也就意味着所有的Java对象都拥有Object类的属性和方法。
如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。
范例:使用Object接收所有类的对象
class Person {
}
class Student {
}
public class Test {
public static void main(String[] args) {
function(new Person());
function(new Student());
}
public static void function(Object obj) {
System.out.println(obj);
}
}
toString()方法
Object类的toString()的作用:
方法的原理:
现在,使用toString方法的时候,打印出来的东西 “不好看”,对于其他人来说不友好,可读性不好
我们现在是想知道对象的信息,名字,年龄,身高。。。。。。
现在的格式不好:
出现的问题:子类Student对父类Object提供的toString方法不满意,不满意--》对toString方法进行重写:
public class Student /*extends Object*/{
private String name;
private int age;
private double height;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public Student() {
}
public Student(String name, int age, double height) {
this.name = name;
this.age = age;
this.height = height;
}
public String toString() {
return "这是一个Student对象,这个对象的名字:"+name+",年龄:"+age+",身高:"+height;
}
}
测试类:
总结:toString的作用就是对对象进行“自我介绍”,一般子类对父类提供的toString都不满意,都要进行重写。
IDEA提供快捷键生成的:
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
'}';
}
equals方法
public class Phone {//手机类:
//属性:
private String brand;//品牌型号
private double price;//价格
private int year ;//出产年份
//方法:
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
@Override
public String toString() {
return "Phone{" +
"brand='" + brand + '\'' +
", price=" + price +
", year=" + year +
'}';
}
//构造器:
public Phone() {
}
public Phone(String brand, double price, int year) {
this.brand = brand;
this.price = price;
this.year = year;
}
//对equals方法进行重写:
public boolean equals(Object obj) {//Object obj = new Phone();向上转型,父类引用指向子类对象
//将obj转为Phone类型:
Phone other = (Phone)obj;//向下转型,为了获取子类中特有的内容
if(this.getBrand()==other.getBrand()&&this.getPrice()==other.getPrice()&&this.getYear()==other.getYear()){
return true;
}
return false;
}
}
intanceof:
利用集成开发工具生成equals方法:
【1】利用eclipse:
【2】利用idea:
hashCode()方法
hashCode()这个方法,它帮我们算了一个具体的对象位置,目前我们先只能说它是个内存地址。
然后调用Integer.toHexString()方法,将这个地址以16进制输出。
hashcode方法源码:
public native int hashCode();
该方法是一个native方法,底层是由C/C++代码写的。我们看不到。
我们认为两个名字相同,年龄相同的对象,将会存储在同一个位置,如果不重写hashcode()方法,我们可以来看示例代码:
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Test {
public static void main(String[] args) {
Person per1 = new Person("themyth", 22);
Person per2 = new Person("themyth", 22);
//我们希望这两个对象 放到同一块位置上
System.out.println(per1.hashCode());
System.out.println(per2.hashCode());
}
}
注意事项:两个对象的hash值不一样!
像重写equals方法一样,我们也可以重写hashcode()方法。此时我们再来看看。
import java.util.Objects;
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Test {
public static void main(String[] args) {
Person per1 = new Person("themyth", 22);
Person per2 = new Person("themyth", 22);
//我们希望这两个对象 放到同一块位置上
System.out.println(per1.hashCode());
System.out.println(per2.hashCode());
}
}
注意事项:哈希值一样。
结论:
1、hashcode方法用来确定对象在内存中存储的位置是否相同
2、事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
类和类的关系
通过代码更直观地来理解:
public class Girl {
//属性:
String name;
double weight;
Mom m /*= new Mom()*/;//这儿不用赋值,在测试类中通过调用女孩的属性来赋值一个妈妈。
//方法:
public void add(int a){//参数是基本数据类型
System.out.println(a);
System.out.println(a+100);
}
//谈恋爱的方法:
public void love(Boy b){//参数是引用数据类型Boy
System.out.println("我男朋友的名字是:"+b.name+",我男朋友的年龄是:"+b.age);
b.buy();
}
//女孩跟妈妈聊天:
public void wechat(){
m.say();
}
//构造器:
public Girl(String name, double weight) {
this.name = name;
this.weight = weight;
}
}
public class Boy {
//属性:
int age;
String name;
//方法:
public void buy(){
System.out.println("跟我谈恋爱,我给你买买买。。。");
}
//构造器:
public Boy(int age, String name) {
this.age = age;
this.name = name;
}
}
public class Mom {
//方法:
public void say(){
System.out.println("妈妈唠唠叨叨 都是爱,听妈妈的话。。");
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//创建一个Boy类的具体的对象:
Boy boy = new Boy(30,"鹿晗");
//创建一个Girl类的具体的对象:
Girl girl = new Girl("关晓彤",100);
//谈恋爱:
//girl.love(boy);
Boy boy2 = new Boy(35,"陈伟霆");
girl.love(boy2);
//还可以跟妈妈微信聊天:
girl.m = new Mom();
girl.wechat();
}
}
总结:
【1】面向对象的思维:找参与者,找女孩类,找男孩类
【2】体会了什么叫方法的形参,什么叫方法的实参:
具体传入的内容实参:
【3】类和类可以产生关系:
(1)将一个类作为另一个类中的方法的形参
(2)将一个类作为另一个类的属性
说明:妈妈放在女孩的属性中,更好一些,因为妈妈是与生俱来的,男朋友今天分了,明天可以换,所以把男朋友放在形参。
对象的组合(计算机硬件)练习:
其中,CPU类要求getSpeed()返回speed的值;要求setSpeed(int m)方法将参数m的值赋值给speed。HardDisk类要求getAmount()返回amount的值,要求setAmount(int m)方法将参数m的值赋值给amount。
PC类要求setCPU(CPU c) 将参数c的值赋值给cpu,要求setHardDisk (HardDisk h)方法将参数h的值赋值给HD,要求show()方法能显示cpu的速度和硬盘的容量。
main方法中创建一个CPU对象cpu,cpu将自己的speed设置为2200,
main方法中创建一个HardDisk对象disk,disk将自己的amount设置为200,
main方法中创建一个PC对象pc,
pc调用setCUP(CPU c)方法,调用时实参是cpu,
pc调用setHardDisk (HardDisk h)方法,调用时实参是disk,
pc调用show()方法。
class CPU {
private int speed;
public int getSpeed() {
return speed;
}
public void setSpeed(int m) {
speed = m;
}
}
class HardDisk {
private int amount;
public int getAmount() {
return amount;
}
public void setAmount(int m) {
amount = m;
}
}
class PC {
private CPU cpu;
private HardDisk HD;
public void setCPU(CPU c) {
this.cpu = c;
}
public void setHardDisk(HardDisk h) {
this.HD = h;
}
public void show() {
System.out.println("CPU速度:" + cpu.getSpeed());
System.out.print("硬盘容量:" + HD.getAmount());
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
CPU cpu = new CPU();
cpu.setSpeed(2200);
HardDisk disk = new HardDisk();
disk.setAmount(200);
PC pc = new PC();
pc.setCPU(cpu);
pc.setHardDisk(disk);
pc.show();
}
}
类和类之间的关系总结:
一、继承关系
继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。在Java中继承关系通过关键字extends明确标识,在设计时一般没有争议性。在UML类图设计中,继承用一条带空心三角箭头的实线表示,从子类指向父类,或者子接口指向父接口。
二、实现关系
实现指的是一个class类实现interface接口(可以是多个)的功能,实现是类与接口之间最常见的关系。在Java中此类关系通过关键字implements明确标识,在设计时一般没有争议性。在UML类图设计中,实现用一条带空心三角箭头的虚线表示,从类指向实现的接口。
三、依赖关系
简单的理解,依赖就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是类B的变化会影响到类A。比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。表现在代码层面,让类B作为参数被类A在某个method方法中使用。在UML类图设计中,依赖关系用由类A指向类B的带箭头虚线表示。
四、关联关系
关联体现的是两个类之间语义级别的一种强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量。在UML类图设计中,关联关系用由关联类A指向被关联类B的带箭头实线表示,在关联的两端可以标注关联双方的角色和多重性标记。
五、聚合关系
聚合是关联关系的一种特例,它体现的是整体与部分的关系,即has-a的关系。此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。比如计算机与CPU、公司与员工的关系等,比如一个航母编队包括海空母舰、驱护舰艇、舰载飞机及核动力攻击潜艇等。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,聚合关系以空心菱形加实线箭头表示。
六、组合关系
组合也是关联关系的一种特例,它体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合。它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束,比如人和人的大脑。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,组合关系以实心菱形加实线箭头表示。
七、总结
对于继承、实现这两种关系没多少疑问,它们体现的是一种类和类、或者类与接口间的纵向关系。其他的四种关系体现的是类和类、或者类与接口间的引用、横向关系,是比较难区分的,有很多事物间的关系要想准确定位是很难的。前面也提到,这四种关系都是语义级别的,所以从代码层面并不能完全区分各种关系,但总的来说,后几种关系所表现的强弱程度依次为:
组合>聚合>关联>依赖。
🟫多态🕵️♂️
认识多态
【1】多态跟属性无关,多态指的是方法的多态,而不是属性的多态。
【2】案例代入:
class Animal {//父类:动物:
public void shout(){
System.out.println("我是小动物,我可以叫。。。");
}
}
class Cat extends Animal{
//喊叫方法:
public void shout(){
System.out.println("我是小猫,可以喵喵叫");
}
public void scratch(){
System.out.println("我是小猫,我可以挠人");
}
}
class Dog extends Animal{
//喊叫:
public void shout(){
System.out.println("我是小狗,我可以汪汪叫");
}
public void guard(){
System.out.println("我是小狗,我可以看家护院,保护我的小主人。。。");
}
}
class Pig extends Animal{
public void shout(){
System.out.println("我是小猪,我嗯嗯嗯的叫");
}
public void eat(){
System.out.println("我是小猪,我爱吃东西。。");
}
}
class Girl {
//跟猫玩耍:
/*public void play(Cat cat){
cat.shout();
}*/
//跟狗玩耍:
/*public void play(Dog dog){
dog.shout();
}*/
//跟小动物玩耍:
public void play(Animal an){
an.shout();
}
}
public class Test {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//具体的猫:--》猫的对象
//Cat c = new Cat();
//具体的小女孩:--》女孩的对象
Girl g = new Girl();
//小女孩跟猫玩:
//g.play(c);
//具体的狗---》狗的对象:
//Dog d = new Dog();
//小女孩跟狗玩:
//g.play(d);
//具体的动物:--》动物的对象:
//Cat c = new Cat();
//Dog d = new Dog();
Pig p = new Pig();
Animal an = p;
g.play(an);
}
}
【3】总结:
(1)先有父类,再有子类:--》继承 先有子类,再抽取父类 ----》泛化
(2)什么是多态:
多态就是多种状态:同一个行为,不同的子类表现出来不同的形态。
多态指的就是同一个方法调用,然后由于对象不同会产生不同的行为。
(3)多态的好处:
为了提高代码的扩展性,符合面向对象的设计原则:开闭原则。
开闭原则:指的就是扩展是开放的,修改是关闭的。
注意:多态可以提高扩展性,但是扩展性没有达到最好,反射时候再细讲
(4)多态的要素:
一,继承: Cat extends Animal ,Pig extends Animal, Dog extends Animal
二,重写:子类对父类的方法shout()重写
三, 父类引用指向子类对象:向上转型
Pig p = new Pig();
Animal an = p;
将上面的代码合为一句话:
Animal an = new Pig();
=左侧:编译期的类型
=右侧:运行期的类型
Animal an = new Pig();
g.play(an);
public void play(Animal an){//Animal an = an = new Pig();向上转型
an.shout();
}
上面的代码,也是多态的一种非常常见的应用场合:父类当方法的形参,然后传入的是具体的子类的对象,然后调用同一个方法,根据传入的子类的不同展现出来的效果也不同,构成了多态。
内存分析
多态例子的练习👀:
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃鱼~~~");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃骨头~~~");
}
}
public class TestAnimal {
public static void eat(Animal an) {
an.eat();
}
public static void main(String[] args) {
eat(new Cat("小猫咪", 2));
eat(new Dog("小汪", 1));
}
public static void main3(String[] args) {
//传参之前就已经发生向上转型
/*Cat cat = new Cat("小猫咪", 2);
Dog dog = new Dog("小汪", 1);*/
Animal cat = new Cat("小猫咪", 2);
Animal dog = new Dog("小汪", 1);
eat(cat);
eat(dog);
}
public static void main2(String[] args) {
//传参的时候再发生向上转型
Cat cat = new Cat("小猫咪", 2);
Dog dog = new Dog("小汪", 1);
eat(cat);
eat(dog);
}
//这是一个main方法,是程序的入口:
public static void main1(String[] args) {
Cat cat = new Cat("小猫咪", 2);
Dog dog = new Dog("小汪", 1);
cat.eat();
dog.eat();
}
}
多态的优缺点
案例引入:
class Shape {
//属性....
public void draw() {
System.out.println("画图形!");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("❀");
}
}
public class TestShape {
public static void drawMap(Shape shape) {//多态写法
shape.draw();
}
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
Shape rect = new Rect();//传参前向上转型
Cycle cycle = new Cycle();//传参时向上转型
drawMap(rect);
drawMap(cycle);
drawMap(new Flower());
}
}
使用多态的好处:
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ? 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解.
而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构. 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .
例如我们现在需要打印的不是一个形状了, 而是多个形状.
如果不基于多态, 实现代码如下:
public class TestShape {
public static void drawMap() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
//● ♦ ● ♦ ❀
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if ("cycle".equals(shape)) {
cycle.draw();
} else if ("rect".equals(shape)) {
rect.draw();
} else {
flower.draw();
}
}
}
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
drawMap();
}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.
public class TestShape {
public static void drawMap() {
/*Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();*/
/*Shape rect = new Rect();
Shape cycle = new Cycle();
Shape flower = new Flower();
//● ♦ ● ♦ ❀
Shape[] shapes = {rect, cycle, rect, cycle, flower};*/
Shape[] shapes = {new Rect(), new Cycle(), new Rect(), new Cycle(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
drawMap();
}
}
2. 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawMap方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawMap 中的 if - else 进行一定的修改, 改动成本更高.
多态缺陷:代码的运行效率降低。
1. 属性没有多态性 当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性
避免在构造方法中调用重写的方法
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
public D() {
super();
}
@Override
public void func() {
System.out.println("D.func() " + num + " 因为此时父类还没有走完!");
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
向下转型,向上转型
通过案例来引入:
class Animal {//父类:动物:
int age;
public void shout(){
System.out.println("我是小动物,我可以叫。。。");
}
}
class Pig extends Animal {
double weight;
public void shout(){
System.out.println("我是小猪,我嗯嗯嗯的叫。。。");
}
public void eat(){
System.out.println("我是小猪,我爱吃东西");
}
}
现在就想访问到eat()方法和weight属性:
public class Demo {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
Pig p = new Pig();
Animal an = p;//转型:向上转型
an.shout();
//加入转型的代码:
//将Animal转为Pig类型:
Pig pig = (Pig)an ;//转型:向下转型
pig.eat();
pig.age = 10;
pig.weight = 60.8;
}
}
对应内存:
思考之前的equals方法:
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
向上转型练习(工厂设计模式):
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃鱼~~~");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃骨头~~~");
}
}
public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eatFood(Animal a) {
a.eat();
}
// 3. 作返回值:返回任意子类对象
public static Animal buyAnimal(String var) {
if ("狗".equals(var)) {
return new Dog("狗狗", 1);
} else if ("猫".equals(var)) {
return new Cat("猫猫", 1);
} else {
return null;
}
}
public static void main(String[] args) {
Animal cat = new Cat("小猫咪", 2); // 1. 直接赋值:子类对象赋值给父类对象(父类引用指向子类对象)
Dog dog = new Dog("小汪", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
}
向上转型练习(员工薪资问题):
class Employee { //公司员工类
double earnings() { //计算年薪的方法
return 0;
}
}
class YearWorker extends Employee { //按年领薪水的员工
//重写earnings()方法,年薪200000
@Override
double earnings() {
return 200000;
}
}
class MonthWorker extends Employee { //按月领薪水的员工
//重写earnings()方法,按月薪12000计算年薪,计12个月
@Override
double earnings() {
return 12 * 12000;
}
}
class WeekWorker extends Employee { //按周领薪水的员工
//重写earnings()方法,按周薪3500计算年薪,计52周
@Override
double earnings() {
return 52 * 3500;
}
}
class Company {
Employee[] employee;
Company(Employee[] e) {
employee = e;
}
double salariesPay() {
//计算所有员工的总年薪
double salaries = 0;
for (int i = 0; i < employee.length; i++) {
salaries = salaries + employee[i].earnings();
}
return salaries;
}
}
public class CompanySalary {
public static void main(String args[]) {
Employee[] e = new Employee[23]; //声明公司有23名雇员
//创建10名年薪制雇员(0~9号),并计算他们的年薪总支出
double sum = 0;
for (int i = 0; i < 10; i++) {
e[i] = new YearWorker();
sum += e[i].earnings();
}
System.out.println("年薪制雇员的薪水总支出:" + sum + "元");
//创建8名月薪制雇员(10~17号),并计算他们的年薪总支出
sum = 0;
for (int i = 10; i < 18; i++) {
e[i] = new MonthWorker();
sum += e[i].earnings();
}
System.out.println("月薪制雇员的薪水总支出:" + sum + "元");
//创建5名按周计薪雇员(18~22号),并计算他们的年薪总支出
sum = 0;
for (int i = 18; i < 23; i++) {
e[i] = new WeekWorker();
sum += e[i].earnings();
}
System.out.println("按周计薪雇员的薪水总支出:" + sum + "元");
Company company = new Company(e);
System.out.println("本年度公司薪水总支出:" + company.salariesPay() + "元");
}
}
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
向下转型:将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
向下转型练习:
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃鱼~~~");
}
public void mew() {
System.out.println(name + ":喵喵喵~~~");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃骨头~~~");
}
public void bark() {
System.out.println(name + ":汪汪汪!!!");
}
}
public class TestAnimal {
public static void main(String[] args) {
Animal animal1 = new Cat("小猫咪", 2);
// 编译失败,编译时编译器将animal1当成Animal对象处理,animal2同理
// 而Animal类中没有bark方法,因此编译失败
// animal1.bark();
//animal本来指向的就是猫,因此将animal还原为猫也是安全的
/*Cat cat = (Cat)animal;
cat.mew();*/
((Cat)animal1).mew();
Animal animal2 = new Dog("小汪", 1);
//animal本来指向的就是狗,因此将animal还原为狗也是安全的
/*Dog dog = (Dog)animal2;
dog.bark();*/
((Dog)animal2).bark();
}
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。
例如:我们将猫的对象转换成狗的对象时候就会出现异常。
Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃鱼~~~");
}
public void mew() {
System.out.println(name + ":喵喵喵~~~");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "吃骨头~~~");
}
public void bark() {
System.out.println(name + ":汪汪汪!!!");
}
}
public class TestAnimal {
public static void main(String[] args) {
/*Cat xiaomaomi = new Cat("xiaomaomi", 2);
Animal animal = xiaomaomi;
if (animal instanceof Cat) {//animal是不是Cat类的一个实例,是返回true,否则返回false(animal是否引用了Cat的一个对象)
xiaomaomi = (Cat)animal;
xiaomaomi.mew();
}
Dog xiaowang = new Dog("xiaowang", 1);
animal = xiaowang;
if (animal instanceof Dog) {//animal是不是Dog类的一个实例,是返回true,否则返回false(animal是否引用了Dog的一个对象)
xiaowang = (Dog)animal;
xiaowang.bark();
}*/
Animal animal = new Cat("小猫咪", 2);
if (animal instanceof Cat) {
((Cat) animal).mew();
}
animal = new Dog("小汪", 1);
if (animal instanceof Dog) {
((Dog) animal).bark();
}
}
}
简单工厂设计模式
不仅可以使用父类做方法的形参,还可以使用父类做方法的返回值类型,真实返回的对象可以是该类的任意一个子类对象。
简单工厂模式的实现,它是解决大量对象创建问题的一个解决方案。将创建和使用分开,工厂负责创建,使用者直接调用即可。简单工厂模式的基本要求是
1️⃣定义一个static方法,通过类名直接调用
2️⃣返回值类型是父类类型,返回的可以是其任意子类类型
3️⃣传入一个字符串类型的参数,工厂根据参数创建对应的子类产品
public class Test {
public static void main(String[] args) {
Girl g = new Girl();
//Cat c = new Cat();
//Dog d = new Dog();
//Pig p = new Pig();
Animal an = PetStore.getAnimal("狗");
g.play(an);//可以将两句合成一句->g.play(PetStore.getAnimal("狗"));
}
}
public class PetStore {//宠物店 ---》工厂类
//方法:提供动物
public static Animal getAnimal(String petName){//多态的应用场合(二)
Animal an = null;
if("猫".equals(petName)){//petName.equals("猫") --》这样写容易发生空指针异常
an = new Cat();
}
if("狗".equals(petName)){
an = new Dog();
}
if("猪".equals(petName)){
an = new Pig();
}
return an;
}
}