目录
(2)默认情况下,equals比较的是对象(地址)是否相同。
一、包
管理类的文件夹
1、什么是包?
在一个Java工程项目中,为了方面管理各种不同的类,避免每个程序员写的类名发生混淆,所以把各个类放在包中管理。如图:
而包在操作系统中的体现就是文件夹,可以说
包就是文件夹。当你创建了一个包,就在操作系统里创建了一个文件夹。
2、包的命名
包命名与类命名类似,可以包含数字、字母、下划线、圆点,但不能以数字开头,也不能命名为关键字。
如:将包命名为public时,则不符合规则,因为是关键字。(不能在其中创建Java类)
二、访问修饰符
1、访问修饰符有哪些?
(1)public
访问权限最大,拥有public的属性或者方法,可以被同类、同包、子类和不同包的类访问。
(2)protected
可以被同类、同包、子类的类访问。不同包的类不可访问。
(3)默认(什么都不写)
可以被同类、同包的类访问。子类和不同包的类不可访问。
(4)private
只能被本类访问。
举例:如图用四种不同的访问修饰符,定义了四种不同的属性:
将其放在Example01包下:
我们在Example01类访问一下:
可见,n4由于是私有属性,不可被其他类访问。
整理为表格:
三、继承
类继承类继承类
1、什么是继承?
为了让程序的维护和书写更加方便,诞生了继承。
如图,我定义了一个动物类:
这里的age年龄和walk走路,都是动物都有的行为或属性。
我现在想定义一个Cat类,可见,猫类的属性也应该有age,行为也应该有walk,
但是如果再写一遍的太麻烦了,所以我们就可以让Cat类继承Animal
这样,Cat也可以访问Animal的属性,而程序员基于这样的特性就减少了很多代码量。
2、注意事项:
(1)Java是单继承机制,一个类只能继承一个父类。
(2)一个类的顶级父类是Object类(这是Java自带的类,是所有类的顶级父类)
(3)一个类被继承后,这个类也可以去继承别的类。(就是一个类成为父类后,其也可以再去继承别的类,但依然是只能继承一个类)
3、构造器与继承
当一个类A继承了另一个类B后,他们调用构造器会有这样一个机制。
当子类调用子类的构造器时,会先调用父类的构造器(如果这个父类还继承了一个类,那么会先调用这个父类的父类的构造器。。)
如图:创建两个无参构造器。
创建子类Cat对象cat,这时会调用子类的构造器。
运行一下:发现父类的构造器先被调用,然后才是子类的构造器
原因就是在于底层中子类的构造器隐藏着一个super(),也就是调用父类的无参构造器
这就是为什么在创建子类对象时,会默认调用父类的无参构造器。
而super的含义就是父类,类似于this指的是本类。super()就是调用父类无参构造器。
而由于这个规则:创建子类对象时,会默认调用父类的无参构造器。
所以,子类A要继承父类B,那么父类就必须要有无参构造器。
总结下来就一句话:
创建子类时,会先调用父类的无参构造器,然后调用子类的构造器。
4、super关键字:
(1)访问父类属性
super关键字代指当前类的父类。
例如:现在我想在cat类中调用父类的age属性,而不是子类的age属性。
就可以使用super关键字来访问:
输出试一下,确实输出的是Animal.age:
(2)构造器中的super
构造器中的super只能出现在第一行,且只能调用一次。如果不在构造器中手动调用super(),那么系统会默认在第一行加入一个隐藏的super()。(跟上面讲的一样)
构造器中也可以使用this(),也是只能出现在第一行,所以显然super()和this()不可同时用。
如果父类有多个构造器,可以用super()指定一个构造器使用。
如:super()就是用无参构造器(默认也是用无参)
super(1)这里就是有参构造器了,在Animal类中指的是将1赋值给age:
5、内存中的继承(重要)
来看下面这段代码,你觉得会输出什么?
package Example01;
public class Example01 {
public static void main(String[] args) {
Son son = new Son();
System.out.println(son.name);
System.out.println(son.age);
System.out.println(son.hobby);
}
}
class GrandPa{
int age = 70;
String hobby = "打篮球";
}
class Father extends GrandPa{
int age = 45;
String name = "爸爸";
}
class Son extends Father{
int age = 18;
String name = "儿子";
}
这里son并没有hobby这个属性,所以编译器会自动的往他的父类及父类的父类查找,最终找到他的超类(父类的父类)hobby属性为止。
在内存中是这样运行的:
首先由于GrandPa类是Son的超类,在创建son对象时,会先创建GrandPa的属性:
age = 70 hobby = “打篮球”
然后是父类的属性:
最后是son自己的属性:
同时,在方法区内还会有这些类的继承关系的查找关系:
有了类之间继承的查找关系,编译器就可以找到到底代码值得是哪个属性。
在这个例子中,在主方法中想要打印出son.age属性, age属性是GrandPa Father 和Son类都有的,编译器从最底层的Son类开始查找,结果发现有age属性,并且已经被赋值,那么就会直接把age打印出来。
然后打印hobby属性,hobby属性是son没有的属性,但是无妨,编译器会先从Son类查找,发现没有hobby属性,于是编译器继续往Son的父类Father查找,发现Father也没有hobby属性,于是再往Father的父类GrandPa开始查找,最终找到了hobby,然后就会把GrandPa的这个hobby属性作为son.hobby来打印出来。
所以最终运行结果就是这样的:
四、方法的重写(覆盖)
1、什么是重写?
重写就是子类的方法与父类的一个方法名相同,这样就会覆盖掉父类的方法,如图就是重写:
这里爸爸有一个玩篮球的方法,如果儿子没有重写(覆盖)爸爸的这个玩篮球方法,调用这个方法就是调用的爸爸的玩篮球方法,输出:“爸爸玩篮球”,而儿子这里也可以重写这个玩篮球的方法,这样就会覆盖掉原来爸爸的玩篮球方法,从而输出:“儿子玩篮球”
运行结果:
2、为什么要有重写?
为了提高代码的复用性。
上面这个玩篮球的例子并没有很好的体现重写让代码复用性提升,所以再举一个例子
package Example01;
class Shape {
public void draw() {
System.out.print("画");
}
}
class Circle extends Shape {
@Override
public void draw() {
super.draw();
System.out.println(" 圆形");
}
}
class Square extends Shape {
@Override
public void draw() {
super.draw();
System.out.println(" 方形");
}
}
public class Main {
public static void main(String[] args) {
Shape shape1 = new Circle();
Shape shape2 = new Square();
shape1.draw(); // 输出:画 圆形
shape2.draw(); // 输出:画 方形
}
}
这里用到了Shape(形状)的draw方法的重写来体现,Circle(圆形)类和Square(方形)类都继承了Shape类,但是Circle(圆形)类和Square(方形)类的draw的方式必然是不一样的,一个是画圆,一个是画方,所以它们需要重写draw方法,来让draw方法来与自己一致。
class Circle extends Shape {
@Override
public void draw() {
super.draw();
System.out.println(" 圆形");
}
}
这里在第一行使用super.draw()这样就可以既使用父类Shape的draw方法,又同时也带有自己的一些独特的方法操作。
输出:
3、重写的使用细节
(1)方法重写不可以缩小父类方法的访问权限
如图,重写时缩小访问权限会报错。
(2)重写方法时,方法的形参列表方法名称必须完全相同
(3)子类方法的返回值应该与父类的返回值相同或者应该是父类返回值的子类
如图,这里先创建一个方法,返回值是父类Shape的一个对象:
现在让子类重写这个方法,但返回的是Circle对象:
可见,编译也可以通过。
学习重写时,注意与方法重载对比学习
五、封装
就是把代码包装到方法里,从而加强代码的可读性和可维护性。
六、多态
在java中方法和属性由于继承和封装的特性,往往体现出多种状态。
1、方法的多态
重写和重载都体现了方法的多态。
(1)重载:方法既可以没有参数列表,也可以有多个参数列表,返回值也可以不同。
(2)重写:方法可以在不同的类中有不同的表现形式,在Shape类中就是画一个形状,
而在Circle方法中就是画一个圆。
2、对象的多态(重难点)
(1)注意事项:
一个对象的编译类型和运行类型可以不同。
如图 a的编译类型是Animal而运行类型是Cat (等号左:编译类型 等号右:运行类型)至于为什么要这么做,而编译类型和运行类型是什么意思,看后面。
编译类型在创建对象时就已经确定了,不可改变
从上图解释就是a的编译类型永远是Animal不可改变。
运行类型可以变化
如图我在Animal类中写一个cry(叫)的方法,并在子类Cat类中重写:
现在我在main主方法中创建对象a,a的编译类型是Animal而运行类型是Cat,然后我调用a的cry方法,问题:输出的是“动物叫”还是“小猫叫”呢?
这就涉及到对象多态的方法调用的机制问题。
(2)对象多态的方法调用机制
首先,编译器看到a想要调用cry方法,会先通过编译类型来看a是否有cry方法。
a的编译类型是Animal,Animal类中是有cry方法的,所以编译器就会认为a有cry方法,可以调用。
但是注意,这里只是可以调用,而并没有确定调用哪一个cry方法,别忘了Animal的子类Cat类也是有一个cry方法的。那么到底调用哪一个cry方法就要看运行类型了。
a的运行类型是Cat,所以最终运行的cry方法运行的是Cat类重写过的cry方法。
结果:
总结下来就是:
编译类型决定可不可以运行方法,运行类型决定运行哪一个方法
如果编译类型没有这个cry方法,编译器就识别不到,就不可以运行这个方法。
如图:
但是这样又产生了一个问题,如果子类Cat没有重写cry方法,那么会发生什么,编译器会报错吗?
不会。如果发现运行类型Cat没有cry方法,编译器会自动往它的父类查找这个方法,直到找到cry方法为止。
举例:
运行结果:
根据以上几点进行总结可以知道:
a可以调用父类(Animal)的所有成员(方法cry、属性),而不可以调用子类(Cat)的特有成员。
(子类的特有成员就是父类中不存在的,所以编译器无法在父类里找到,所以会被判断为不可以运行)
(3)向上转型
向上转型大家已经见过了:
Animal a = new Cat();
这种把父类的引用指向子类的对象的语句,就被称为向上转型。
这样会使a的编译类型与运行类型不一致,从而导致上面所说的对象的多态。
(4)向下转型
上面说过,向上转型不可以调用子类的特有成员,那么有没有方法调用子类的特有方法呢?
有,向下转型。
Cat b = (Cat)a;
将a强行转换为Cat 并让新引用b指向a,然后再用b调用Cat的特有方法就可以了。
如图:定义一个Cat的特有方法。
将a向下转型到b:
可以看到,b调用Cat的特有方法eat时,就不会报错了。
注意事项:
只能够转型父类的引用,而不是父类对象。
Animal a = new Cat();
可以看到,a只是一个父类的引用,并不是一个对象本身,真正的对象时这个new出来的Cat,a只是指向了这个对象而已,并不是真正的对象。
而我们向下转型时,转的是这个引用a,而不是对象。
父类的引用转型的目标必须是其指向的对象的运行类型
这里a的运行类型是Cat,所以向下转型时必须是(Cat)a而不是(Dog)a或(Animal)a。
(5)属性的调用机制
属性的调用直接看编译类型,与运行类型无关。
运行输出:
(6)instanceOf运算符
instanceOf是一种运算符(注意不是方法),可以判断对象是否是某一个类或某一个类的子类,返回布尔值:
那么instanceOf判断的究竟是编译类型还是引用类型呢?
答案是引用类型。记住就行。
3、动态绑定机制
之前其实也提到了,就是方法的调用机制与对象相绑定。先看编译类型,有没有这个方法,有就进一步判断,没有则不可以执行。编译类型有这个方法,那就根据运行类型执行这个方法,运行类型没有就找运行类型的父类执行方法。
4、多态的应用(重要)
(1)多态数组
上一篇说过,创建一个类就是创建了一个数据类型,所以我们可以创建一个Animal数组:
新建两个对象,并把他们放入Animal数组——animals中。
我们这时想要依次调用animals中的cry方法。使用for循环:
由于animals中存放的都是Animal或者其子类,所以其必然可以调用cry方法,所以这里直接写了
animals[i].cry()
直接调用。
而又由于动态绑定机制,所以cat的cry方法与dog的cry方法必然也是不同的。
运行结果:
(2)多态参数
方法的参数是一个类时,传入的参数也可以是这个类的子类对象
例如这里我定义一个Test类,并创建一个方法,方法调用传入的animal的cry方法:
使用这个方法时,也可以传入Animal类的子类。
结果:
七、Object类
所有类的顶级父类,所有类创建时自动继承
有一些在里面定义的方法值得学习
1、equals()方法
(1)equals()只能比较引用类型
基本数据类型不可以
(2)默认情况下,equals比较的是对象(地址)是否相同。
相同返回一个true,不同则返回false。
(3)可以在类中重写equals方法,便于判断。
其中一个比较经典的例子就是String重写的equals方法,让equals比较字符串本身,而不是对象。
比较String如果使用“==”则会发生错误:
如下图所示,因为==比较的是对象,而String重写的equals比较的是字符串本身,所以输出了如下结果。(str1和str3不是同一个对象)
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
System.out.println(str1 == str2); // 输出:true
System.out.println(str1 == str3); // 输出:false
System.out.println(str1.equals(str3)); // 输出:true
再举一个例子,我在cat中重写了equals方法:
class Cat extends Animal{
int age;
String name;
//重写equals方法
@Override
public boolean equals(Object o) {
//如果是同一个对象 直接返回true
if (this == o) return true;
//如果不是Cat类 或者其子类,直接返回false
if (!(o instanceof Cat)) return false;
//由于传入的是Object类,这里向下转型才能访问cat的name和age
Cat cat = (Cat) o;
//如果age和name都一样,返回true 否则false
return age == cat.age &&
Objects.equals(name, cat.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
//构造器
public Cat(int age, String name) {
this.age = age;
this.name = name;
}
public Cat() {
}
}
创建三个对象,并使用equals判断,可以看到,虽然cat1和cat2不是同一个对象,但是只要是他们的age和name相同,那么我就让equals判断为true。
public class Example03 {
public static void main(String[] args) {
Cat cat1 = new Cat(3, "Tom");
Cat cat2 = new Cat(3, "Tom");
Cat cat3 = new Cat(2, "Jerry");
System.out.println(cat1.equals(cat2)); // 输出:true
System.out.println(cat1.equals(cat3)); // 输出:false
}
}
重写equals必须也重写hashcode的计算方法,而至于hashcode,现在姑且可以把其等价于地址,一个地址有且仅有一个hashcode与其对应,但是hashcode并不是真正的地址,这个以后再说,先有个印象就行。
2、toString()方法
很简单,当我们想要输出一个对象的属性时,我们可以一个一个用“.”打出来,也可以直接输入对象,此时系统会自动调用toString方法打印。为了格式更清楚,我们可以重写toString方法,来让其输出我们想输出的格式。
举例:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 重写toString方法,返回表示对象的字符串表示形式
*/
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public static void main(String[] args) {
// 创建一个Person对象
Person person = new Person("John", 25);
// 打印person对象,会自动调用toString方法
System.out.println(person); // 输出:Person{name='John', age=25}
}
}
这里我们直接在System.out.println()中输入person对象,这时会直接按照toString方法的格式打印。
3、finalize方法
当没有引用指向对象时,我们就没有办法来再次访问这个对象,这个对象就相当于迷失了。
(就像宇航员在没有任何链接措施时走向太空 这样宇航员不可能再回到太空舱 迷失了)
这时,java程序会自动帮我们回收这个对象,从而释放内存资源。
而当对象被回收时,会自动调用finalize方法。
这时程序员就可以来重写finalize方法,来实现一些业务。
后面说异常这一章的时候可以体现。
就整理到这里把,重新回顾这里自己也是有一些新体会,还是不错的。