封装
封装就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法),才能对数据进行操作。
实现步骤
-
将属性进行私有化private
-
提供一个公共的set方法,用于对属性判断并赋值
-
public void setXXX(类型 参数){ 属性 = 参数名 }
-
-
提供一个公共的get方法,用于获取属性的值
-
public XX getXX(){ return xx }
-
顺带一提,快捷键alt+R第一次来跑一段代码的时候,因为没有设置主类,所以跑的可能是上一个程序,此时只需要先用鼠标右键run一次,界面的右上角的主类就会变化为当前的类,之后就可以直接用快捷键跑了;
继承
d目的:解决代码的复用性问题,让编程更加接近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends来声明父类即可
子类又叫派生类,父类又叫超类、基类
继承相关的细节问题
-
子类继承了所有的属性和方法,非私有的属性和方法可以直接访问,但是私有属性和方法不能在子类直接访问,要通过父类提供的公共的方法去访问
-
子类必须调用父类的构造器,完成父类的初始化
- 补充一点,这里涉及到了super(),而super()其实是默认存在的,它的存在默认调用父类的无参构造器
-
当创建子类对象时,不管使用子类的哪个构造器,默认情况下总是会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用super去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过。
-
public class Parent { // 带参数的构造器 public Parent(String message) { System.out.println("Parent constructor with message: " + message); } // 无参构造器 public Parent() { System.out.println("Parent default constructor"); } } public class Child extends Parent { // 子类的无参构造器 public Child() { // 默认会调用父类的无参构造器 System.out.println("Child default constructor"); } // 子类的带参数构造器 public Child(String message) { // 需要显式调用父类的带参数构造器,否则会默认调用父类的无参构造器 super(message);//这一步如果不显式地说明则会编译不通过 System.out.println("Child constructor with message: " + message); } } public class Main { public static void main(String[] args) { // 创建子类对象,调用子类的无参构造器 Child child1 = new Child(); // 创建子类对象,调用子类的带参数构造器 Child child2 = new Child("Hello"); } } //输出结果 Parent default constructor Child default constructor Parent constructor with message: Hello Child constructor with message: Hello
-
-
如果希望指定去调用父类的某个构造器,则显式地调用一下: super(参数列表)
-
super在使用时,必须放在构造器第一行(super只能在构造器中使用)
-
super()和this()都只能放在构造器第一行,因此这两个方法不能共存在一个构造器中
-
Java中所有类都是Object类的子类,Object是所有类的基类
-
父类构造器的调用不限于直接父类,将一直往上追溯直到Object类(顶级父类)
-
子类最多只能继承一个父类(指直接继承),即Java中是单继承机制
- 思考:如何让A类继承B类和C类? A继承B,B继承C即可
-
不能滥用继承,子类和父类之间必须满足is-a的逻辑关系
继承本质详解
子类创建的内存布局,务必清楚
class A{
A(){
System.out.println("a");
}
A(String name){
System.out.println("a name");
}
}
class B extend A{
B(){
//此中有this就没super的哈
this("abc");
System.out.println("b");
}
B(String name){
//super()这里潜藏着一个super的哈
System.out.println("b name");
}
}
main方法中,B b = new B();会输出什么呢?
result: // a b name b
解析:this.abc先调用本类中的带参数构造器,于是跳到B(String name),在这个构造器中首行,有个默认的super(),会去调用父类A的无参构造器,于是先输出a,然后回去,输出b name,最后B(String name)执行完后,回到B的无参构造器中this("abc")之后,打印出b
//此中的重点就是你要分析出来B(String name)中隐藏的super(),而B中无参构造器中有this就没super了
所有的构造器,如果没有显式地声明super()或this()之类的,务必记住默认会有个super(),默认调用父类的无参构造器
super关键字
//1.访问父类的属性,但不能访问父类的private属性 super.属性名
//2.访问父类的方法,不能访问父类的private方法 super.方法名(参数名)
//3.访问父类的构造器(之前也提到过),super(参数列表); 只能放在构造器的第一句,只能出现一句!
super 给编程带来的便利
1.调用父类构造器的好处(分工明确,父类属性由父类初始化,子类属性由子类初始化)
2.当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过super。
如果没有重名,使用super、this、直接访问是一样的效果
3.super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员;
如果多个基类(上级类)中都有同名的成员,使用super访问遵循就近原则。 A->B—>C 当然也需要遵守访问权限等相关规则
super和this的区别
练习题 强烈建议走一遍
方法重写/覆盖
方法覆盖(重写)就是子类有一个方法,和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的那个方法。 注意:这里的子类父类不一定是直接的父子关系哈,祖孙关系也算的哈
/**
1.子类方法的形参列表、方法名称、要和父类方法的参数、方法名称完全一样
2.子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类
比如父类的返回类型是Object ,子类方法返回类型是String ,此中String是Object 的子类,所以也是允许的
Dad类: public Object m(){}
Son类: public String m(){}
这两也是构成重写的
3.子类方法不能缩小父类方法的访问权限 public > protected > 默认 > private
重写和重载的区别
多态*
多态的前提是:两个对象存在继承关系
具体体现:
1.方法重载、方法重写体现多态
2.对象的多态(核心)
/**重要的几句话:
1.一个对象的编译类型和运行类型可以不一致
Animal animal = new dog(); animal的编译类型是Animal 运行类型是dog;
animal = new cat(); animal的编译类型还是Animal 但运行类型此时变为了cat;
2.编译类型在定义对象时,就确定了,不能改变
3.运行类型是可以改变的
4.编译类型看定义时 = 号的左边,运行类型看 = 号的右边
*/
多态注意事项及细节讨论
/**
1.多态的前提是:两个对象(类)存在继承关系
2.多态的向上转型:
2.1本质:父类的引用指向了子类的对象
2.2语法: 父类类型 引用名 = new 子类类型();
2.3特点:编译类型看左边,运行类型看右边
可以调用父类中的所有成员(需遵守访问权限)
---比如你把原先父类的一个eat方法由public改为private,那么就无法通过animal.eat()来调用;
不能调用子类中特有成员
---比如Cat类中有个特有的catchMouse的方法,animal.catMouse();这行代码就会报错;
---这是因为在编译阶段,能调用哪些成员,是由编译类型来决定的
最终运行效果看子类(运行类型)的具体实现,即调用方法时,按照从子类开始查找方法,然后调用,规则之前已
经提到过,就是从子类出发,如果没有找到所指定的方法,则层层往上找;
---animal.eat();//这行代码首先去看animal的运行类型eat中去找有没有eat()这个方法,发现Cat这个类重写
了eat()方法,小猫吃鱼,那么就会输出小猫吃鱼,反之则到父类animal中去执行其eat()方法,输出“动物
吃食物";
---animal.run();//这行代码也是一样,但此时发现Cat类中找不到run()这个方法,那么此时就会到父类Animal
类中去找到run()这个方法去调用,输出"动物在奔跑";
3.多态的向下转型:
3.1语法: 子类类型 引用名 = (子类类型)父类引用
3.2只能强转父类的引用,不能强转父类的对象
3.3要求父类的引用必须指向的是当前目标类型的对象
---就是cat.catMouse();这行代码要是能正常跑起来,要求你之前就已经写了Animal animal = new Cat();
使得父类引用animal指向了猫Cat类型的对象;
---试问,如果此时有个Dog类也是继承Animal类,但是直接写Dog dog = (Dog)animal;这行代码有毛病吗?
答案:是有的,你之前就没有向上转型,使父类引用指向此时的目标类型的对象Dog,编译时是不会报错的,
但跑起来,就会发生ClassCaastException异常,即类转换异常,性质相当于,你可以说猫是动物,狗是动物,
但不能说狗是猫,因为Cat cat = (Cat)animal;cat.catMouse;这两行代码执行无异常的本质前提是,之前
已经写了Animal animal = new Cat();使父类引用指向了子类对象,建立了这个前提;
3.4当向下转型后,可以调用子类类型中所有的成员
--- Cat cat = (Cat)animal;
cat.catMouse();//这时候,这行代码经过向下转型,就可以调用子类的特有方法catMouse了,不会报错;
此时,编译类型是Cat,运行类型也是Cat;
*/
Animal animal = new Cat();
Object obj = new Cat();
System.out.println("OK~");//代码到这儿也是可以执行的,即上面两行代码都是不会报错的
/**
4.属性没有重写之说!属性的值看编译类型
--父类Base有属性count=10,子类Sub也有属性count=20;
--Base base = new Sub();//向上转型
--System.out.println(base.count);//此时的输出就要看编译类型Base,所以最后输出的就是10,而不是20;
--Sub sub = new Sub();System.out.println(sub.count); // 此时输出的就是20,而不是10,还是一样看编译类型
5.instanceOf 比较操作符,用于判断对象的[运行]类型是否为XX类型或XX类型的子类型
--AA是父类,BB是子类,extends AA
--BB bb = new BB();
--sout.(bb instanceof BB); //输出 true
--sout.(bb instanceof AA); //输出还是 true
--AA aa = new BB();
--sout.(aa instanceof AA);//输出 true
--sout.(aa instanceof BB);//输出还是true 所以可得其判断的是对象的运行类型是否为XX类型或xx类型的子类型
--因为如果看编译类型,则aa 是AA类,不是BB的子类,第二句输出的应该是false,假设看运行类型,aa是BB类,
那么,aa instanceof BB 为true,aa instance AA 也为true,因为BB extends AA ;
--Object obj = new Object();
--sout.(obj instanceof AA);//此时这里输出的是什么呢? 答案是false,因为看运行类型Object是AA的
父类,父亲怎么会和儿子等同呢?倒反天罡!!
--String str = "hello";
--sout.(str instanceof Object);//输出是true,想对了吗? 因为String的父类是Object,所以儿子instanceof父亲,
没毛病,如果反过来,object instance String;那就false
*/
Object obj = "Hello";//向上转型,父类型引用指向子类型对象
String objStr = (String)obj;//向下转型
Object objPri = new Integer(5);//向上转型,没问题
String str = (String)objPri;
//错误,报出ClassCastException,类转换异常,虽然是在尝试向下转型,但因为objPri上一句已经指向了Integer类,
//联想之前的猫狗问题,objPri已经和Integer产生羁绊,你个String又怎能来插手二者的感情呢?
Integer str1 = (Integer)objPri;//正确的,之前Object和Integer已经产生了羁绊,此时再向下转型,进行转化,也是允许的
Java的动态绑定机制
//1.当调用对象【方法】的时候,该方法会和该对象的【内存地址/运行类型】绑定
//2.当调用对象【属性】的时候,【没有动态绑定机制】,哪里声明,哪里使用
未使用动态绑定机制,要清楚结果怎么得到的:
使用动态绑定机制后,要理解结果怎么得到的
此中的30是因为先找B类的sum()方法,找不到,然后追溯到A类的sum()方法,然后,此时的getI()方法的调用者是A类对象a,其运行类型是B类型对象,所以,会去调用B类中的getI()方法,得到20,然后回去得到最终的结果30
多态应用
多态数组
多态参数
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getAnnual(){
return 12*salary;
}
}
public class worker extends Employee{
public worker(String name, double salary) {
super(name, salary);
}
public void work(){
System.out.println("工人"+getName()+"在工作");
}
@Override
public double getAnnual() {
return 12*getSalary();
}
}
public class manager extends Employee{
private double bonus;
public manager(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}
public double getBonus() {
return bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
@Override
public double getAnnual() {
return super.getAnnual()+bonus;
}
public void manage(){
System.out.println("经理"+getName()+"正在管理员工");
}
}
public class Test {
public static void main(String[] args) {
worker wok1 = new worker("A ", 2000);
manager manager = new manager("Jason", 5000, 10000);
Test test = new Test();
test.showEmpAnnual(wok1);//24000.0
test.showEmpAnnual(manager);//70000.0
test.testWork(wok1);//工人A 在工作
test.testWork(manager);//经理Jason正在管理员工
}
public void showEmpAnnual(Employee employee){
System.out.println(employee.getAnnual());
}
public void testWork(Employee e){
if(e instanceof worker){
((worker) e).work();//有向下转型的操作, e.work直接快捷生成
}else if(e instanceof manager){
((manager) e).manage();
}else{
System.out.println("你输入的类型有误,不做处理");
}
}
}
equals方法
//equals 和 ==之间的对比
==是一个比较运算符,既可以判断基本类型,又可以判断引用类型
如果判断基本类型,判断的是值是否相等
如果判断引用类型,判断的是地址是否相等,即判断是不是同一个对象
equals是Object类中的方法,只能判断引用类型;
默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等
System.out.println("abc".equals("abc"));
以下这是两个字符串通过equals来判断是否相等的底层equals方法的源码逻辑实现
public boolean equals(Object anObject) {
if (this == anObject) {//如果是同一个对象,返回true
return true;
}
if (anObject instanceof String) {//判断类型,如果不是字符串,直接就返回false
String anotherString = (String)anObject;//向下转型
int n = value.length;//获取传入参数的长度
if (n == anotherString.value.length) {//如果二者长度相同,则进一步判断是否真的相等
char v1[] = value;//两个各自转成char数组
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {//进入while循环,逐个char数组的元素来比较是不是全部相等
if (v1[i] != v2[i])
return false;
i++;
}
return true;//如果两个字符串的所有字符都相等,则返回true
}
}
return false;
}
这是Object的原始equals方法的源代码实现
public boolean equals(Object obj) {
return (this == obj);
}
此中默认比较的是否是一个对象,比较内存地址是否相同,但是结合实际,我们一般需要对此进行改写;
这是Integer的equals方法,同样,也可以看到对原始的Object中的equals进行了改写,变成了判断两个值是否相同
public boolean equals(Object obj) {
if (obj instanceof Integer) {//判断是不是Integer类型的对象
return value == ((Integer)obj).intValue();//如果是,就比较这个对象对应的int值和传入的值对象的value值是否相等
}
return false;
}
Integer integer1 = new Integer(100);
Integer integer2 = new Integer(100);
sout.(integer1 == integer2);//输出的结果是false
sout.(integer1.equals(integer2));//输出的结果就是true
重写equals方法
需求:判断两个Person对象的内容是否相等,如果两个Person对象的各个属性值都一样,则返回true,反之false
Person p1 = new Person("jack",10,'男');
Person p2 = new Person("jack",10,'男');
//此时如果不改写Perosn的equals方法,则sout(p1.equals(p2)); 输出的是false,因为它会默认调用Object的原始equals方法,
//即直接比较内存地址,判断是不是同一个对象,但是此时我们的预期应当是比较Person的每一个属性,都相等,则返回true,所以要改写
要使得 sout.(p1.equals(p2));
public boolean equals(Object obj){
//判断如果比较的两个对象是同一个对象,则直接返回true
if(this == obj){
return true;
}
//类型判断
if(obj instanceof Perosn){//只有当传入的对象是Person类型的,咱们才比较
//为了得到obj的各个属性,我们需要进行向下转型
Person p = (Person)obj;
//注意,此中的p就是传入的 p2,this指代的就是p1,即下面这行比较p1的各个属性和p2的各个属性是否相等,有一个不等都不行
return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
}
//如果连同一类型都不是,还谈啥相等,直接返回false
return false;
}
顺带一提:sout.("hello" == new java.sql.Date());
//这里的结果不是false哟,而是直接编译报错了,因为二者都不是同一类型,还比啥,直接false;
hashCode方法
关于其的6个小结
1.提高具有哈希结构的容器的效率
2.两个引用,如果指向的是同一个对象,则哈希值肯定是一样的
3.两个引用,如果指向的是两个不同的对象,则哈希值是绝大部分情况下是不一样的,哈希碰撞时,可能会发生一样的情况
4.哈希值主要是根据地址号来的,不能完全的将哈希值等价于地址
toString方法
默认返回:全类名+@+哈希值的16进制;
Object的子类往往重写 toString方法,用于返回对象的属性信息
Object 的toString方法源码如下
public String toString() {
//全类名就是指 包名 + 类名
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
打印对象或拼接对象时,都会自动调用该对象的toString形式
当直接输出一个对象时,toString方法会被默认调用
finalize方法
当垃圾回收器确定不存在对该对象的更多引用时,由该对象的垃圾回收器调用此方法
1.当对象被回收时,系统自动调用该对象的finalize方法,子类可以重写此方法,程序员如有需要,就可以重写,在其中
实现释放资源,比如关闭文件,断开数据库的连接,这些资源的释放
2.什么时候被回收:当某个对象没有任何引用时,则JVM就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该
对象前,会先调用finalize方法
3.垃圾回收机制的调用,是由系统决定的,即有它自己的gc回收算法,也可以通过System.gc()主动触发垃圾回收机制;
//补充, system.gc(),并不会立即执行,如果你在这个语句之后还有sout.(xxx)语句,会先等其执行完再执行
//在实际开发中,我们几乎不会运用finalize方法,更多是为了应对面试
断点调试
//重要提示:在断点调试的过程中,是运行状态,是以对象的运行类型来执行的
A extends B ; B b = new A(); b.xx();
//在调试b.xx()方法时,是按照它的运行类型A来定位这个方法的
断点调试快捷键
F7(跳入)F8(跳过)
F7:跳入方法内
shift + F8(跳出)
F9(resume,执行到下一个断点)
shift+F8:逐行执行代码
各个访问修饰符的权限范围
看组多态相关代码练习
//向上转型
Person p = new Student();
那么此时的p可以调用的方法有父类的run和eat,因为未运行之前看的是编译类型Person;
p.run();
p.eat();
但实际运行的时候,会执行动态绑定机制,p.run()这行代码执行的时候,看到运行类型是Student
就到子类Student去找 run() 方法,找到了,最后输出student run;而第二句 p.eat();
同样执行动态绑定机制,先去子类找eat方法,但没找到,所以去看父类,发现父类有,就输出person eat;
//向下转型------把指向子类对象的父类引用,转成指向子类对象的子类引用
//务必记住,向下转型的前提是已经有向上转型,建立了绑定关系,不然就会出现猫是狗,狗是猫的情况
Student s = (Student)p;
此时Student s = (Student)p;
此时的s可以调用Student类的run和study方法,当然,因为Student继承Person类,那么父类的eat方法也是可以调用的;
s.run();//student run
s.study();//student study
s.eat();//person eat
==和equals的区别
super和this的练习题
回顾复习多态
多态是什么?多态的具体体现呢?
多态:方法或对象具有多种形态,是OOP的三大特征,是建立在封装和继承的基础之上;
多态具体体现
- 方法多态
- 重载体现多态
- 重写体现多态
- 对象多态
- 对象的编译类型和运行类型可以不一致,编译类型在定义时,就确定,不能变化
- 对象的运行类型是可以变化的,可以通过getClass()来查看运行类型;
- 编译类型看定义时,=号的左边,运行类型看 = 号的右边
- 举例说明如下
Java动态绑定机制回顾复习
- 当调用对象的方法时,该方法会和对象的内存地址/运行类型绑定
- 当调用对象的属性时,没有动态绑定机制,哪里声明,哪里使用