面向对象编程
面向对象编程有三大特征:封装、继承和多态
类的五大成员:(1) 属性 (2) 方法 (3) 构造器 (4) 代码块 (5) 内部类
访问修饰符
Java
一共提供了四种访问控制修饰符,用于控制方法和属性(成员变量)的访问权限(范围):
public
:公开级别,对外公开protected
:受保护级别,对子类和同一个包中的类公开- 默认级别:没有修饰符号,向同一个包类公开
private
:私有级别,只有类本身可以访问,不对外公开
注意事项:
- 修饰符可以用来修饰类中的属性,成员方法以及类
- 只有默认的和
public
修饰符才能修饰类,其他两个修饰符不能修饰类,并且遵循上述访问权限的特点 - 成员方法的访问规则和属性完全一样
对于类中的四个属性,分别使用不同的访问修饰符进行修饰:
package com.jlctest.modifier;
public class A {
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;
// 本类的方法是可以访问到上述不同修饰符的属性
public void m1() {
System.out.println(n1, n2, n3, n4);
}
}
在同包中创建一个B类:
package com.jlctest.modifier;
public class B {
A a = new A(); // 同一个包中,不需要对同包下的类进行引入
// 在同一个包下,可以访问 public、protected和默认的属性或方法,但是不能访问私有的属性或方法
System.out.println(a.n1, a.n2, a.n3); // a.n4访问不到
}
在不同包中创建一个C类:
package com.jlctest.modifier2;
public class B {
A a = new A(); // 同一个包中,不需要对同包下的类进行引入
// 在不同包下,只能访问 public修饰的属性或方法,不能访问protected、默认的和私有的属性或方法
System.out.println(a.n1); // a.n2 a.n3 a.n4访问不到
}
封装
封装就是把抽象出的数据(属性)和对数据操作的方法封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法),才能对数据进行操作。对电视机的封装操作就是典型的封装,开关等按键被暴露出来了
封装有以下的好处:
- 隐藏了实现细节,只需在调用时传递参数即可
- 可以对数据进行验证(对于一些非常不合理的数据进行验证处理,如
age = 1000;
,显然是不合理的),保证安全合理
封装步骤
将属性进行私有化
private
,在外部不能直接的修改属性提供一个公共(
public
)的set
方法,用于属性判断并赋值javapublic void setXxx(类型 参数名) { // xxx表示某个属性 // 加入数据验证的业务逻辑 属性 = 参数名; }
提供一个公共(
public
)的get
方法,用于获取属性的值javapublic XX getXxx() { // xxx表示某个属性 // 权限判断 return xx; }
封装小案例:封装一个Person
类,不能随便查看人的年龄,工资等隐私,并对设置的年龄进行合理的验证(1-120岁之间),年龄合理就设置,否则使用默认的年龄,工资不能直接查看,name
的长度在2-6个字符之间,具体封装如下:
package com.jlctest.encap;
public class Encapsulation01 {
Person person = new Person();
person.setName("jlc");
person.setAge("25");
person.setSalary(20000);
// 不能通过person.salary进行访问当前对象的薪水,只能通过person.getSalary()进行薪水的访问
// 读取信息
System.out.println(person,info());
}
class Person {
public String name; // 名字公开
private int age; // 年龄私有化
private double salary; // 工资私有化
// 给每个属性提供set和get方法,可以使用快捷键alt+insert,选择Getter and Setter
// 之后根据要求来完善代码
public void setName(String name) {
// 对名字长度的校验
if (name.length() >= 2 && name.length() <= 6) {
this.name = name;
}
else {
System.out.println("名字长度不合法");
// 设置默认的名字
this.name = "null";
}
}
public String getName() {
return name;
}
public void setAge(int age) {
// 合理范围判断
if (age >= 1 && age <= 120) {
this.age = age;
}
else {
System.out.println("年龄要在1-120之间");
// 不符合范围设置给定的默认值
this.age = 18;
}
}
public int getAge() {
return age;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
// 可以在这里增加对当前对象的权限判断
return salary;
}
// 写一个方法,返回属性信息
public String info() {
return "信息为: name:" + name + "age:" + age + "salary:" + salary;
}
}
封装结合构造器
使用构造器会导致封装的过滤校验机制失效,如果我们不希望校验方法失效,我们可以将set
方法在构造器中进行调用,如:
package com.jlctest.encap;
public class Encapsulation01 {
Person person = new Person();
person.setName("jlc");
person.setAge("25");
person.setSalary(20000);
// 不能通过person.salary进行访问当前对象的薪水,只能通过person.getSalary()进行薪水的访问
// 读取信息
System.out.println(person,info());
// 通过构造器初始化
Person person1 = new Person("JLC", 125, 20000);
System.out.println(person1,info()); // JLC 18 20000
}
class Person {
public String name; // 名字公开
private int age; // 年龄私有化
private double salary; // 工资私有化
// 使用构造器
public Person(String name, int age, double salery) {
//// 下面方式会使验证失效
//this.name = name;
//this.age = age;
//this.salary = salary;
// 如果需要用到验证,需要调用set方法
this.setName(name);
this.setAge(age);
this.setSalary(salary);
}
// 给每个属性提供set和get方法,可以使用快捷键alt+insert,选择Getter and Setter
// 之后根据要求来完善代码
public void setName(String name) {
// 对名字长度的校验
if (name.length() >= 2 && name.length() <= 6) {
this.name = name;
}
else {
System.out.println("名字长度不合法");
// 设置默认的名字
this.name = "null";
}
}
public String getName() {
return name;
}
public void setAge(int age) {
// 合理范围判断
if (age >= 1 && age <= 120) {
this.age = age;
}
else {
System.out.println("年龄要在1-120之间");
// 不符合范围设置给定的默认值
this.age = 18;
}
}
public int getAge() {
return age;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
// 可以在这里增加对当前对象的权限判断
return salary;
}
// 写一个方法,返回属性信息
public String info() {
return "信息为: name:" + name + "age:" + age + "salary:" + salary;
}
}
继承
对于两个类的属性和方法,有很多是相同的,这个时候,我们就需要用到继承的概念,实现代码的复用性,同时代码的扩展性和维护性提高了
继承可以解决代码的复用,让我们的编程更加靠近人类的思维,当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends
来声明继承父类即可,继承的示意图如下:
B
类和C
类中有很多属性和方法是相同的,我们将B
类和C
类中共有的属性写在A
类中,供B
类和C
类进行继承,在B
类和C
类中只写该类特有的属性和方法,我们可以在写一个类D
来继承B
类,这样D
类就同时拥有了B
类和A
类的所有方法
继承的基本语法:
class 子类 extends 父类 {
...
}
- 子类会自动拥有父类定义的属性和方法
- 父类又叫超类或基类
- 子类又叫派生类
代码描述:创建一个学生的父类,和小学生子类、大学生子类,父类文件代码:
package com.jlctest.extend;
// 父类
public class Student {
// 共有的属性
public String name;
public int age;
private double score;
// 共有的方法
public void setScore(double score) {
this.score = score;
}
public void showInfo() {
System.out.println("学生名:" + name + "年龄:" + age + "成绩:" + score);
}
}
创建子类进行继承父类和编写子类独特的属性和方法:
package com.jlctest.extend;
// 小学生子类 Pupil继承Student父类
public class Pupil extends Student {
public void testing() {
System.out.println("小学生" + name + "正在考试...");
}
}
package com.jlctest.extend;
// 大学生子类 Graduate继承Student父类
public class Graduate extends Student {
public void testing() {
System.out.println("大学生" + name + "正在考试...");
}
}
测试文件:
package com.jlctest.extend;
public class Extends01 {
public static void main(String[] args) {
com.jlctest.extend.Pupil pupil = new Pupil();
pupil.name = "abc";
pupil.age = 12;
pupil.testing();
pupil.setScore(60);
pupil.showInfo();
}
}
注意事项:
子类继承了所有的属性和方法,但是私有属性不能在子类中直接访问(但是可以间接访问的),要通过父类提供的公共方法去访问
代码解释:先创建一个父类
Base
:javapackage com.jlctest.extend; public class Base { public int n1 = 100; // 公共属性 protected int n2 = 200; // 受保护属性 int n3 = 300; // 默认属性 private int n4 = 400; // 私有属性 // 无参构造器 public Base() { System.out.println("Base..."); } // 公共方法 public void test100() { System.out.println("test100..."); } // 受保护方法 protected void test200() { System.out.println("test200..."); } // 默认方法 void test300() { System.out.println("test300..."); } // 私有方法 private void test400() { System.out.println("test400..."); } // 父类提供一个公共的方法去返回私有的属性 public int getN4() { return n4; } // 父类提供一个公共的方法去返回私有的方法 public void callTest400() { test400(); } }
创建一个子类
Sub
继承父类Base
:javapackage com.jlctest.extend; public class Sub extends Base { // 子类的构造器 public Sub() { System.out.println("Sub..."); } public void sayOk() { // 非私有的属性和方法可以在子类中直接访问 System.out.println(n1, n2, n3); // n4是私有属性,不能直接访问 // 通过父类提供的公共方法去访问私有的属性 System.out.println(getN4()); test100(); test200(); test300(); // test400()是私有方法,不能直接访问 // 通过父类提供的公共方法去访问私有的方法 callTest400(); } }
子类必须调用父类的构造器,完成父类的初始化
在创建子类的时候,父类的无参构造器也会被调用,会先于子类的无参构造器调用(先完成对父类的初始化)
在子类中有一个默认的被省略的语句
super();
,完整的子类构造器语句为:javapackage com.jlctest.extend; public class Sub extends Base { // 子类的构造器 public Sub() { super(); // 默认调用父类的无参构造器 System.out.println("Sub..."); } }
当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用
super
去指定父类的哪个构造器完成对父类的初始化工作,否则编译不会通过javapackage com.jlctest.extend; public class Base { public int n1 = 100; // 公共属性 protected int n2 = 200; // 受保护属性 int n3 = 300; // 默认属性 private int n4 = 400; // 私有属性 // 有参构造器,覆盖了默认的无参构造器 public Base(int n1) { System.out.println("Base(int n1)..."); } }
子类在调用的时候,需要指定父类中具体的构造器:
javapackage com.jlctest.extend; public class Sub extends Base { // 子类的构造器 public Sub() { super(150); // 指定一个构造器合法的实例,显示的调用一下 System.out.println("Sub..."); } }
如果希望指定去调用父类的某个构造器,则显式的用
super()
调用一下,如果不写,则默认调用父类的无参构造器super
在使用时,必须放在构造器第一行super()
和this()
都只能放在构造器第一行,因此这两个方法不能共存在一个构造器中Java
所有的类都是Object
类的子类,Object
是所有类的基类父类构造器的调用不限于直接调用上级的父类,可以一直往上追溯到
Object
类(顶级父类),但是注意Object
的无参构造器是没有任何输出的子类最多只能继承一个父类(指直接继承),即
Java
中是单继承机制的如果想要让
A
类继承B
类和C
类,我们可以让A
类先继承B
类,再让B
类去继承C
类不能滥用继承,子类和父类之间必须满足
is-a
(是一个...)的逻辑关系Person is a Music?
人不是音乐,不合理,不应该是继承关系
练习题:
public class ExtendsExercise01 {
public static void main(String[] args) {
B b = new B(); // 执行内容:a b name b
}
}
class A {
A() {
System.out.println("a");
}
A(String name) {
System.out.println("a name");
}
}
class B extends A {
B() {
this("abc"); // 执行B(String name){}
System.out.println("b");
}
B(String name) {
// 默认为super(); // 执行A()
System.out.println("b name");
}
}
继承的本质
继承的本质可以帮助我们理解,当子类继承父类时,创建子类对象时,内存中发生了什么(结论:当子类对象创建好后,内存中会建立查找关系)
public class ExtendsTheory {
public static void main(String[] args) {
Son son = new Son(); // new一个Son时,内存到底发生了什么,内存的布局是怎么样的
// 当我们使用实例出的子类去访问属性时,是根据什么规则呢?
System.out.println(son.name); // 大头儿子
System.out.println(son.age); // 45 子类没有,访问的父类的属性
System.out.println(son.hobby); // 旅游 子类父类都没有,继续往上找
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {
String name = "大头爸爸";
int age = 45;
}
class Son extends Father {
String name = "大头儿子";
}
new
一个Son
类的时候,首先会加载类信息,在加载Son
类信息的时候,会先加载顶层的父类信息,也就是Object
类,之后依次加载GrandPa
的类信息和Father
的类信息,等父类的信息加载完后,最后才会加载Son
的类信息加载完类信息之后,会在堆中分配地址空间,首先会给爷爷类分配属性;再会开辟一个空间,为爸爸类分配属性,最后为子类自己开辟空间,分配属性
当我们使用实例出的子类去访问属性时,是根据什么规则呢?我们需要按照查找关系来返回数据,查找规则如下:
- 首先看子类是否有目前要访问的属性,如果子类有这个属性,并且可以访问,则返回信息
- 如果子类没有这个属性,就看父类有没有这个属性,如果父类有这个属性,并且可以访问,就返回信息
- 如果父类没有这个属性,就继续往上找,直到
Object
类
如果父级中的一个属性是私有的,那么子类是不可以进行直接访问的(但是这个私有的属性在内存地址中还是存在的),只是我们需要通过公共的方法进行访问
另外,如果爸爸类中有一个age
是私有的,爷爷类中也有一个age
是公共的,那我们子类访问age
属性会卡在爸爸类中(直接访问报错,即有一个堵住了,不会跳过这个继续往上查找),不会继续往爷爷类去查找(即使爷爷类中的age
是私有的)
super
关键字
super
代表父类的引用,用于访问父类的属性、方法和构造器
super
可以访问父类的属性(访问方式:super.属性名;
),但不能访问父类的私有属性super
可以访问父类的方法(访问方式:super.方法名(参数列表);
),但不能访问父类的私有方法super
可以访问父类的构造器(访问方式:super(参数列表);
),只能放在构造器的第一句
package com.jlctest.super;
// 父类
public class A {
// 四个属性
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private n4 = 400;
// 四个方法
public void test100() {}
protected void test200() {}
void test300() {}
private void test400() {}
}
package com.jlctest.super;
// 子类
public class B extends A {
public void h1() {
System.out.println(super.n1, super.n2, super.n3); // super.n4不能访问,报错
}
public void ok() {
super.test100();
super.test200();
super.test300();
// super.test400();不能访问,报错
}
}
super
关键字可以给编程带来便利:
- 调用父类的构造器的好处:分工明确,父类属性由父类初始化,子类的属性由子类初始化
- 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过
super
(super
访问属性和方法时,是没有查找本类该属性和方法的过程,直接去父类中进行查找,这个和this
和直接访问是有区别的);如果没有重名,使用super
、this
和直接访问是一样效果的 - 对于父类中私有的属性和方法,访问会报错:
cannot access
;如果父类中都没有这个属性和方法,会提示属性或方法不存在 super
的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super
去访问爷爷类的成员,如果多个基类(上级类)中都有同名的成员,使用super
访问遵循就近原则。当然也需要遵循访问权限的相关规则(如私有属性和方法是不能访问的)
super
和this
的比较
方法重写
方法重写也叫方法覆盖,就是子类有一个方法和父类(这里的父类可以一直追溯到顶级父类)的某个方法的名称、返回类型、形参列表都一样,那么我们就说子类的这个方法覆盖了父类的方法
对于一个Animal
父类中,有一个方法:
package com.jlctest.override;
public class Animal {
public void cry() {
System.out.println("动物叫..");
}
}
对于子类Dog
中,也有cry
这个方法:
package com.jlctest.override;
public class Dog extends Animal {
public void cry() {
System.out.println("狗叫..");
}
}
因为
Dog
是Animal
的子类,Dog
的cry
方法和Animal
的cry
方法的定义形式一样(名称、返回类型和形参列表),这时我们就说Dog
的cry
方法重写了Animal
的cry
方法
在一个测试文件中进行测试:
package com.jlctest.override;
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.cry(); // 狗叫..
}
}
方法重写的注意事项:
子类的方法的形参列表,方法名称要和父类方法的形参列表,方法名称和返回类型完全一样,会出现方法的重写
在方法名称和形参列表一样的基础上,如果父类方法的返回类型是子类方法返回类型的父类,也会构成方法的重写(如:父类方法返回的类型是
Object
,子类方法返回的类型是String
)String
类型的本质就是Object
,如果子类方法返回的类型是Object
,父类方法返回的类型是String
,系统会直接报错对于方法的重写,子类方法不能缩小父类方法的访问权限(可以扩大,但是不能缩小)
对于访问修饰符的权限,由大到小依次为:
public
>protected
>默认>private
如果父类的方法是
public
,那么子类的方法必须为public
,否则报错如果父类的方法是
protected
,那么子类的方法可以为protected
和public
对于重写后的方法,如果我们想要重新使用子类的方法,我们可以使用
super.父类方法()
的形式调用
方法重写和方法重载的比较:
多态
多态,通俗来讲就说多种状态,是面向对象的第三大特征,多态是建立在封装和继承的基础上的
多态可以提高我们代码的复用性,从而利于维护
方法的多态
重写和重载就体现了方法的多态
package com.jlctest.Ploy;
public class Ploy {
public static void main(String[] args) {
B b = new B();
A a = new A();
// 方法的重载体现多态:这里我们传入不同的参数,就会调用不同的sum方法,就体现多态
System.out.println(a.sum(10, 20)); // 30
System.out.println(a.sum(10, 20, 30)); // 60
// 方法重写体现多态:
a.say(); // A say
b.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");
}
}
对象的多态
对象多态的前提是两个对象(类)存在继承关系
对象的多态是多态的核心:
- 一个对象的编译类型和运行类型可以不一致(可以使用父类的引用指向子类的对象)
- 编译类型在定义对象时,就确定了,不能改变
- 运行类型是可以变化的
- 编译类型看定义时等号的左边,运行类型看等号的右边
// Dog类是Animal类的子类
// 可以使用父类的引用指向子类的对象
Animal animal = new Dog();
animal = new Cat(); // animal的运行类型变成了Cat,但是编译类型仍然是Animal
其中
animal
的编译类型是Animal
,运行类型是Dog
// animal的编译类型是Animal,运行类型是Dog
Animal animal = new Dog();
animal,cry(); // 因为运行时,执行到该行时,animal的运行类型是Dog,所以这里的cry方法,就是Dog类中的方法
// 将animal的运行类型改为Cat,但是编译类型还是为Animal
animal = new Cat();
animal.cry(); // 运行类型为Cat,执行的是Cat类中的cry方法
运行类型可以理解为真实的当前类型
多态的向上转型
本质:父类的引用指向了子类的对象
语法:
父类类型 引用名 = new 子类类型();
特点:编译类型看左边,运行类型看右边
可以调用父类中的所有成员(需遵循访问权限)
不能调用子类中特有成员
最终运行的效果看子类的具体实现
javaAnimal animal = new Cat(); // 父类的引用指向了子类的对象 // 实例出的animal的类型完全是由编译器决定的,遵循的是引用类型Animal animal.*; // 可以调用父类中的所有成员方法(属性和方法)(需遵循访问权限,私有的成员不能访问) // 但是不能调用子类中特有的成员(即子类有父类没有的成员) animal.catchMouse(); // 报错catchMouse()是Cat类特有的方法 // 最终运行的效果看子类的具体实现,调用方法时,按照从子类开始进行查找,子类没有才往父类进行查找 animal.eat(); // 根据运行类型,调用Cat类中的eat()方法(如果子类中有这个方法的情况下) animal.run(); // 如果子类中没有run()这个方法,才会去父类中进行找
在编译阶段,能调用哪些成员(属性和方法),是由编译类型决定的
但是在最终运行的时候,还是要看子类实现具体的效果
多态的向下转型
- 语法:
子类类型 引用名 = (子类类型) 父类引用;
(将一个父类的引用强制转换为一个子类的引用,使其可以调用子类中的特有方法) - 只能强转父类的引用,不能强转父类的对象
- 要求父类的引用必须指向的是当前目标类型的对象
- 当向下转型后,就可以调用子类类型中的所有成员
Animal animal = new Cat(); // 父类的引用指向了子类的对象
animal.catchMouse(); // 报错catchMouse()是Cat类特有的方法(我们使用向下转型来解决)
// 向下转型,强转,将父类的引用重写转换为子类的引用
Cat cat = (Cat) animal; // 这个时候cat的编译类型和运行类型都是Cat
// 要求父类的引用必须指向的是当前目标类型的对象,也就是说animal原先的指向类型就是Cat类
// 转换完后,就可以调用子类的特有方法
cat.catchMouse();
Animal animal = new Cat(); // 父类的引用指向了子类的对象
// 要求父类的引用必须指向的是当前目标类型的对象,也就是说animal原先的指向类型就是Cat类
Cat cat = (Cat) animal;
Dog dag = (Dog) animal; // 报错,没有满足父类的引用必须指向的是当前目标类型的对象
属性没有重写
属性没有重写,属性的值看其编译类型
package com.jlctest.poly;
public class PloyDetail {
public static void main(String[] args) {
Base base = new Sub(); // 向上转型
System.out.println(base.count); // 10 看编译类型,编译类型为Base
Sub sub = new Sub();
System.out.println(base.count); // 20 看编译类型,编译类型为Sub
}
}
// 父类
class Base {
int count = 10; // 父类的count属性设置为10
}
// 子类
class Sub extends Base {
int count = 20; // 子类的count属性设置为20
}
instanceOf
比较操作符
instanceOf
比较操作符,用于判断对象的运行类型是否为XX
类型或者XX
类型的子类型
package com.jlctest.poly;
public class PloyDetail {
public static void main(String[] args) {
// 运行类型和编译类型都为BB
BB bb = new BB();
System.out.println(bb instanceOf BB); // true
System.out.println(bb instanceOf AA); // true
// 运行类型为BB 编译类型为AA
AA aa = new BB();
System.out.println(aa instanceOf AA); // true
System.out.println(aa instanceOf BB); // true
Object obj = new Object();
System.out.println(obj instanceOf AA); // false
}
}
// 父类
class AA {}
// 子类
class BB extends AA {}
动态绑定机制
Java
的动态绑定机制是非常重要的
- 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
- 当调用对象属性时,没有动态绑定机制,哪里声明,哪里调用(在当前的类中查找)
package com.jlctest.dynamicBinding;
public class DynamicBinding {
public static void main(String[] args) {
A a = new B(); // 向上转型
// 调用的对象方法看运行类型
System.out.println(a.sum()); // 40
System.out.println(a.sum1()); // 30
// 如果将子类B中的sum方法注释掉
// 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定,getI()还是从运行类型开始查找
System.out.println(a.sum()); // 20 + 10 = 30
// 如果将子类B中的sum1方法注释掉
// 当调用对象属性时,没有动态绑定机制,哪里声明,哪里调用(在当前的类中查找)
System.out.println(a.sum1()); // 10 + 10 = 20
}
}
// 父类
class A {
pubilc 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;
}
}
多态的应用
多态数组
多态数组:数组的定义类型为父类类型,里面保存的实际元素类型为子类类型
创建一个Person
对象,两个Student
对象和两个Teacher
对象,统一放在多态数组中,并调用每个对象的say
方法
创建Person
父类:
package com.jlctest.ploy;
public class Person {
private String name;
private int age;
// 定义构造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 属性的设置和获取方法
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
// 自定义say方法,返回名字和年龄
public String say() {
return name + "\t" + age;
}
}
创建Student
子类:
package com.jlctest.ploy;
public class Student extends Person {
private double score;
// 定义构造器
public Person(String name, int age, double score) {
super(name, age);
this.score = score;
}
// 属性的设置和获取方法
public void setScore(double score) {
this.score = score;
}
public String getScore() {
return score;
}
// 重写父类的say方法,返回名字和年龄还有分数
public String say() {
return super.say() + "score" + score;
}
// 学生类中特有的方法
public void study() {
System.out.println("学生" + getName() + "正在听课");
}
}
创建Teacher
子类:
package com.jlctest.ploy;
public class Teacher extends Person {
private double salary;
// 定义构造器
public Person(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
// 属性的设置和获取方法
public void setSalary(double salary) {
this.salary = salary;
}
public String getSalary() {
return salary;
}
// 重写父类的say方法,返回名字和年龄还有分数
public String say() {
return super.say() + "salary" + salary;
}
// 老师类中特有的方法
public void teach() {
System.out.println("老师" + getName() + "正在授课");
}
}
使用多态数组进行声明和方法的调用:
package com.jlctest.ploy;
public static void main(String[] args) {
// 创建一个Person对象,两个Student对象和两个Teacher对象,统一放在多态数组中
// Person[]类型的多态数组,其元素子要是Person类和其子类都可以进行存放
Person[] persons = new Person[5];
persons[0] = new Person("jack", 20);
persons[1] = new Student("mary", 18, 80);
persons[2] = new Student("frank", 19, 100);
persons[3] = new Teacher("scoot", 35, 18000);
persons[4] = new Teacher("king", 55, 22000);
// 循环遍历多态数组,调用say方法
for (int i = 0; i < persons.length; i++) {
// 使用了动态绑定机制,编译类型是Person,运行类型会根据实际变化
System.out.println(person[i].say());
// 这里的persons[] 元素的编译类型是Person,是没有办法调用老师类或者学生类中的特有方法的
// persons[i].teach(); // 报错
// 我们要使用向下转型,将编译类型转成具体的子类,再去调用具体的方法
if (persons[i] instanceof Student) { // 先判断运行类型是不是Student类型
Student student = (Student)persons[i];
student.study();
// 上述两条语句可以合二为一: (Student)persons[i].study();
} else if(persons[i] instanceof Teacher) {
Teacher teacher = (Teacher)persons[i];
teacher.teach();
} else if(persons[i] instanceof Person) {
// 不做任何处理
} else {
System.out.println("类型有误");
}
}
}
多态参数
多态参数是指方法定义的形参类型为父类类型,实参类型允许为子类类型
定义员工类Employee
,包含姓名和月工资[private]
,以及计算年工资getAnnual
的方法。普通员工和经理继承了员工,经理类多了奖金bonus
属性和管理manage
方法,普通员工类多了work
方法,普通员工类和经理类要求分别重写getAnnual
方法:
创建Employee
父类:
package com.jlctest.ploy;
public class Employee {
private String name;
private double salary;
// 定义构造器
public Person(String name, double salary) {
this.name = name;
this.salary = salary;
}
// 属性的设置和获取方法
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
return salary;
}
// 自定义年工资方法
public double getAnnual() {
return 12 * salary;
}
}
创建一个普通员工Worker
子类:
package com.jlctest.ploy;
public class Worker extends Employee {
// 定义构造器
public Person(String name, double salary) {
super(name, salary);
}
// 普通员工类中特有的work方法
public void work() {
System.out.println("普通员工" + getName() + "正在工作");
}
// 重写getAnnual方法
public double getAnnual() {
// 因为普通员工没有其他收入,直接调用父类的方法即可
return super.getAnnual();
}
}
创建一个经理Manager
子类:
package com.jlctest.ploy;
public class Manager extends Employee {
private double bonus;
// 定义构造器
public Person(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}
public String getBonus() {
return bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
// 经理类中特有的manage方法
public void manage() {
System.out.println("经理" + getName() + "正在管理");
}
// 重写getAnnual方法
public double getAnnual() {
return super.getAnnual() + bonus;
}
}
在测试类中添加一个方法showEmpAnnual(Employee e)
,实现获取任何员工对象的年工资,并在main
方法中调用该方法[e.getAnnual]
在测试类中添加一个方法testWork
,如果是普通员工,则调用work
方法,如果是经理,则调用manage
方法
package com.jlctest.ploy;
public class PloyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 2000);
Manager milan = new Manager("milan", 5000, 10000);
PloyParameter ployParameter = new PloyParameter();
ployParameter.showEmpAnnual(tom); // 30000
ployParameter.showEmpAnnual(milan); // 70000
ployParameter.testWork(tom); // 普通员工tom正在工作
ployParameter.testWork(milan); // 经理milan正在管理
}
// 获取任何员工对象的年工资的方法
public void showEmpAnnual(Employee e) {
System.out.println(e.getAnnual()); // 使用了动态绑定机制
}
// 添加一个方法testWork,如果是普通员工,则调用work方法,如果是经理,则调用manage方法
public void testWork(Employee e) {
if (e instanceof Worker) {
((Worker) e).work(); // 向下转型
} else if (e instanceof Manager) {
((Manager) e).manage();
}
}
}
Object
根类
Object
是类层次结构的根类,每个类都使用Object
类作为超类,所有对象(包括数组)都可以使用Object
类的方法,Object
类中常用的方法有:
方法 | 描述 |
---|---|
clone() | 创建并返回此对象的一个副本 |
equals() | 指示其他某个对象是否与此对象"相等" |
finalize() | 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法 |
getClass() | 返回此Object 的运行时类 |
hashCode() | 返回该对象的哈希码值 |
toString() | 返回该对象的字符串表示 |
equals
方法
常见面试题:
==
(比较运算符)和equals
的对比:
==
既可以判断基本类型,又可以判断引用类型==
如果判断基本类型,判断的是值是否相等(是判断值相等,而不深入到类型)javaint num1 = 10; double num2 = 10.0; System.out.println(num1 == num2); // ture
==
如果判断引用类型,判断的是地址是否相等,即判断是不是同一个对象javaA a = new A(); A b = a; A c = b; System.out.println(a == c); // true
equals
方法是Object
类中的方法,只能判断引用类型默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等,比如
Integer
、String
中都会将equals
方法进行重写(加入了比较具体的值是否相等)java// Object中的equals方法,默认就是比较对象地址是否相同,即也就是判断两个对象是不是同一个对象 public boolean equals(Object obj) { // 如果是同一个对象,就返回true return (this == obj); } // Integer中的equals方法 public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
javaInteger integer1 = new Integer(1000); Integer integer2 = new Integer(1000); System.out.println(integer1 == integer2); // false 开辟了不同的地址空间 System.out.println(integer1.equals(integer2)); // true 判断具体的值是否相等
如何查看
jdk
原码:- 一般来说
IDEA
配置好JDK
以后,jdk
的源码也就自动配置好了,如果没有,可以点击菜单File
-->Project Structure
-->SDKs
-->Sourcepath
,然后点击右侧绿色的加号,选中我们下载的jdk
子目录下的javafx-src.zip
文件和src.zip
文件 - 在查看某个方法的源码时,将光标放在该方法上,输入
ctrl+b
或者在该方法上点击右键-->go to
-->Declaration or Usages
即可跳转到该方法的源码
- 一般来说
如何重写equals
方法
案例:判断两个Person
对象的内容是否相等,如果两个Person
对象的各个属性值都一样,则返回true
,反之,返回false
package com.jlctest.object;
public class EqualsExercise {
public static void main(String[] args) {
Person person1 = new Person("jack", 10, '男');
Person person2 = new Person("jack", 10, '男');
System.out.println(person1.equals(person2)); // false 继承的是Object中的equals
// 有了自定义的equals方法
System.out.println(person1.equals(person2)); // ture
}
}
class Person {
private String name;
private int age;
private char gender;
// 创建构造器
public Person(String name, int age, char gender) {
this.name = name;
this,age = age;
this.gender = gender;
}
// 重写Object的equals方法
public boolean equals(Object obj) {
// 判断如果比较的两个对象是同一个对象,则直接返回true
if (this == obj) {
return true;
}
// 类型判断,是Person,再进行比较
if (obj instanceof Person) {
// 进行向下转型,获取具体的属性
Person p = (Person)obj;
// 判断全部属性是否相等
return this.name.equals(p.name) && this.age==p.age && this.gender==p.gender;
}
// 如果不是Person对象,则直接返回false
return false;
}
}
hashCode
方法
hashCode
方法用于返回该对象的哈希码值(哈希码值是为了提高哈希表的性能)
该方法主要有以下几个方面:
- 提高具有哈希结构的容器的效率
- 两个引用,如果指向的是同一个对象,则哈希值是一样的
- 两个引用,如果指向的是不同的对象,则哈希值是不一样的
- 哈希值主要根据地址号来计算的,但是不能完全将哈希值等价于地址
- 在后续的集合中,一般会将
hashCode
方法进行重写
package com.jlctest.object;
public class HashCode {
public static void main(String[] args) {
AA aa1 = new AA();
AA aa2 = new AA();
AA aa3 = aa1;
// 此时 aa1.hashCode和aa2.hashCode的值是不一样的,但是与aa3.hashCode的值是一样的
}
}
class AA {}
toString
方法
返回该对象的字符串表示,具体而言,就是返回一个以文本方式表示的此对象字符串
默认返回:全类名(包名+类名)+@+哈希值的十六进制
方法的源码:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
在子类中往往重写toString
方法,用于返回对象的属性信息
当直接输出一个对象时,toString
方法会被默认的调用,如System.out.print(monster);
等价于System.out.print(monster.toString());
finalize
方法
- 当对象被回收时(在堆中的空间被释放出来了),系统自动调用该对象的
finalize
方法,子类可以重写该方法,做一些释放资源的操作 - 什么时候被回收:当某个对象没有任何引用时,则
jvm
就认为这个对象使一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁对象之前,会先调用finalize
方法 - 垃圾回收机制的调用,是由系统来决定的(即有自己的
GC
算法),也可以通过System.gc()
主动触发垃圾回收机制
package com.jlctest.object;
public class Finalize {
public static void main(String[] args) {
Car bmw = new Car("宝马");
// 将对象的指向连线断掉,即对象没有任何引用了,这个Car对象就变成了垃圾,被回收
// 在回收前(销毁前),会调用该对象的finalize方法
// 这时程序员就可以在finalize方法中写一些自己业务逻辑的方法(如释放资源:数据库连接或者打开文件)
// 如果程序员不重写finalize,那么就会调用Object类的finalize,即默认调用
bwm = null;
System.gc(); // 主动触发垃圾回收
System.out.println("程序退出了")
}
}
class Car {
private String name;
publuc Car(String name) {
this.name = name;
}
// 重写finalize方法
@Override
protected void finalize() throws Throwable {
System.out.println("我们销毁了汽车" + name);
}
}
最后结果显示:程序退出了 我们销毁了汽车宝马
finalize
方法在实际的开发中使用的比较少,但是在面试中问的比较多
类变量和类方法
类变量和类方法是面向对象的一个重点,类变量和类方法,又叫静态变量和静态方法,用static
关键字进行修饰
类变量
类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量
定义类变量:
访问修饰符 static 数据类型 变量名;
推荐使用static 访问修饰符 数据类型 变量名;
class Child {
private String name;
// 定义一个类变量为所有的Child类的对象共享
public static int totalNum = 0;
}
类变量的访问:静态变量的访问修饰符的访问权限和范围和普通属性是一样的
类名.类变量名;
推荐使用对象名.类变量名;
类变量可以通过对象进行访问,也可以通过类名进行访问Child.totalNum;
package com.jlctest.static;
public class VisitStatic {
public static void main(String[] args) {
// 类变量是随着类的加载而创建的,所以即使没有创建对象实例也可以访问
System.out.println(A.name); // jlc
// 也可以通过一个对象实例进行访问
A a = new A();
System.out.println(a.name); // jlc
}
}
class A {
// 定义类变量
public static String name = "jlc";
}
类变量的使用细节:
- 当我们需要让某个类的所有对象都共享一个变量时,就可以考虑使用类变量(静态变量)
- 加上
static
就称为类变量\静态变量,否则就为实例变量\普通变量\非静态变量 - 类变量是该类的所有对象共享的,而实例变量(普通属性)是对每个对象独享的
- 实例变量不能通过类名.变量名的方式访问
- 类变量是随着类的加载而创建的,所以即使没有创建对象实例也可以访问
- 类变量的生命周期是随着类的加载开始,随着类的消亡而销毁
内存布局
- 类变量
count
是被该类实例化出来的对象共享的(静态变量是被对象共享的)都指向静态变量所在的空间(堆空间)jdk
的版本如果大于等于8,静态对象是放在堆中的,通过反射机制会加载一个Class
对象(在方法区中类加载之后,会通过反射机制生成一个Class
对象实例),在对象实例的最后,会将这个静态数据放入;jdk
的版本如果小于8,静态对象放在方法区中的,静态变量在加载的时候,有一个类加载,类信息会放在方法区中,就会在方法区中产生一个静态域来存放这个类变量,但是不管静态变量在哪里,都是可以被对象共享的- 类变量,在类加载的时候就生成了
类方法
类方法也叫静态方法
基本语法:
访问修饰符 static 数据返回类型 方法名() {}
推荐使用static 访问修饰符 数据返回类型 方法名() {}
类方法的调用:类名.类方法名
或者对象名.类方法名
调用的前提是满足访问修饰符的访问权限和范围
package com.jlctest.static;
public class StaticMethod {
public static void main(Stringp[] args) {
// 创建两个学生对象,交学费
Stu tom = new Stu("tom");
tom.payFee(1000); // 通过对象来调用静态方法,也可以使用类名进行调用
Stu mary = new Stu("mary");
mary.payFee(2000); // 通过对象来调用静态方法,也可以使用类名进行调用
// 输出当前收到的总学费,通过类进行调用静态方法
Stu.showFee(); // 总学费为:3000.0
}
}
class Stu {
private String name; // 普通私有成员
// 定义一个静态变量,来累积学生的学费
private static double fee = 0;
public Stu(String name) {
this.name = name;
}
// 创建静态方法 静态方法可以访问静态属性/变量
public static void payFee(double fee) {
Stu.fee += fee; // 学费累积
}
public static void showFee() {
System.out.println("总学费为:" + Stu.fee);
}
}
类方法的使用场景:当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率,如工具类中的方法utils
、Math
类、Arrays
类等,如Math.sqrt()
,其中的sqrt()
就是一个静态方法,我们可以直接通过类进行调用(如果我们希望不创建实例,也可以调用某个方法,即当作工具来使用,这时将方法做成静态方法是非常合适的)
在程序员实际开发中,往往会将一些通用的方法,设计成静态方法,这样我们就可以不需要创建对象就直接使用
类方法使用的注意事项:
类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区:类方法中无
this
参数,普通方法隐含着this
参数类方法可以通过类名调用,也可以通过对象名调用
普通方法和对象有关,需要通过对象名调用,不能通过类名调用
类方法中不允许使用和对象有关的关键字,比如
this
和super
,普通方法(成员方法)可以类方法中只能访问静态成员;普通成员方法既可以访问普通成员,也可以访问静态成员(构造器方法也属于普通方法,可以方法普通成员和静态成员)(但是两种方法访问都必须遵守访问权限)
javaclass D { private int n1 = 100; // 普通变量 private static int n2 = 200; // 静态变量 public static void hello() { System.out.println(n2); // 合法 System.out.println(D.n2); // 合法 System.out.println(this.n2); // 不合法,类方法中不允许使用和对象有关的关键字 System.out.println(n1); // 不合法,不能访问普通变量(方法) } }
main
方法
main
方法的语法形式:public static void main(String[] args) {}
main()
方法是Java
虚拟机在调用Java
虚拟机需要调用类的main()
方法,所以该方法的访问权限必须是public
Java
虚拟机在执行main()
方法时不必创建对象,所以该方法必须是static
该方法接收
String
类型的数组参数,该数组中保存执行Java
命令时传递给所运行的类的参数Java
执行的程序 参数1 参数2 参数3javapublic class Hello { public static void main(String[] args) { // 遍历args接收传入的参数 for(int i = 0; i < args.length; i++) { System.out.println("第" + (i + 1) + "个参数为" + args[i]); } } }
编译代码:
javac Hello.java
运行代码并传入参数:
java Hello jlc abc qwer
(如果不传入参数,就什么也不会输出)结果显示:
第1个参数为
jlc
第2个参数为
abc
第3个参数为
qwer
参数在执行程序的时候进行传入,在传入参数后,会将
jlc abc qwer
这三个参数当作一个字符串数组String[]
传给我们的方法
注意事项:
- 在
main()
方法中,我们可以直接调用main
方法所在类的静态方法或静态属性,但是不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态方法
代码块
代码块又称为初始化块,属于类中的成员(即是类的一部分),类似于方法,将逻辑语句封装在方法体中,通过{}
包围起来;但是和方法不同,代码块没有方法名,没有返回,没有参数,只有方法体(代码块可以理解为只有方法体的方法),而且不用通过对象或类显示调用,而是在加载类时,或创建对象时隐式调用
基本语法:
修饰符 {
代码;
};
- 修饰符可选,要写的化,也只能写
static
- 代码块分为两类,使用
static
修饰的静态代码块,和没有static
修饰的普通代码块- 代码块中的语句可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
- 结尾的
;
可以省略
使用代码块的好处:代码块相当于另一种形式的构造器(对构造器的补充机制),可以做初始化的操作
应用场景:如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的复用性
public class CodeBlock {
public static void main(String[] args) {
Movie movie1 = new Movie("movie1");
}
}
class Movie {
private String name;
private double price;
// 2个构造器,构成了重载
// 两个构造器中存在相同的内容,这样代码看起来比较冗余,我们可以将相同的语句,放到一个代码块中
{
System.out.println("电影屏幕打开...");
System.out.println("电影开始...");
};
// 不管我们调用哪个构造器,创建对象,都会调用代码块中的内容
public Movie(String name) {
System.out.println("Movie(String name)构造器被调用");
this.name = name;
}
public Movie(String name, double price) {
System.out.println("Movie(String name, double price)构造器被调用");
this.name = name;
this.price = price;
}
}
结果显示:
电影屏幕打开...
电影开始...
Movie(String name)
构造器被调用
代码块的调用的顺序是优先于我们的构造器调用的
代码块使用注意事项:
static
代码块也叫静态代码块,作用是对类进行初始化,而且它随着类的加载而执行,并且只会执行一次。如果是普通代码块,每创建一个对象,就执行类什么时候被加载(重要):
- 创建对象实例时(
new
) - 创建子类对象实例,父类也会被加载,如果子类和父类中都有代码块,父类的代码块会先被执行
- 使用类的静态成员时(静态属性,静态方法),代码块中的内容会先被执行
- 使用类的静态成员时(静态属性,静态方法),如果这个子类的父类有代码块,也会被加载执行,而且父类的代码块会先被执行
- 创建对象实例时(
普通代码块,在创建对象实例时,会被隐式的调用,被创建一次,就会被调用一次;如果只是使用类的静态成员时,普通代码块并不会执行
创建一个对象时,在一个类调用的顺序是(由先到后):(重点和难点)
调用静态代码块和静态属性初始化
静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按它们定义的顺序调用
javapublic class CodeBlock { public static void main(String[] args) { A a = new A(); // 输出顺序:(1)getN1被调用... (2)A 静态代码块 } } class A { // 静态属性初始化 private static int n1 = getN1(); // 静态代码块 static { System.out.println("A 静态代码块"); } public static int getN1() { System.out.println("getN1被调用..."); return 100; } }
调用普通代码块和普通属性初始化
普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通变量初始化,则按它们定义的顺序调用
调用构造器方法
构造器的代码最前面是隐含了
super()
和调用普通代码块javaclass A { public A() { System.out.println("ok"); } // 上述构造器有隐藏的执行要求 public A() { // (1)调用父类的无参构造器 // 如果父类中也有其本类的普通代码块,在调用父类构造器时,会先进行父类中普通代码块的调用 super(); // (2)调用本类普通代码块的隐藏逻辑 System.out.println("ok"); } }
在创建一个子类对象时(有继承关系),它们的静态代码块,静态属性初始化,普通代码块,普通属性初始化,构造方法的调用顺序为(由先到后):(高频面试题)
- 父类的静态代码块和静态属性(优先级一样,按照定义顺序执行)
- 子类的静态代码块和静态属性(优先级一样,按照定义顺序执行)
- 父类的普通代码块和普通属性初始化(优先级一样,按照定义顺序执行)
- 父类的构造方法
- 子类的普通代码块和普通属性初始化(优先级一样,按照定义顺序执行)
- 子类的构造方法
1和2,是类加载时必须会执行的;后面4步是与对象相关的内容(入口是对应子类的构造器,遵守构造顺序)
静态代码块只能直接调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员
final
关键字
final
关键字可以修饰类、属性、方法和局部变量
有以下的四个情况会使用到final
关键字:
当不希望类被继承时,可以用
final
修饰(希望这个类是最后的类,不希望被其他类继承扩展)java// 如果我们要求A类不能被其他类继承,使用final关键字修饰即可 final class A {} // 这样其他类继承就会直接报错 class B extends A {} // 报错
当不希望父类的某个方法被子类覆盖/重写时,可以使用
final
关键字修饰java// 如果我们不希望父类的某个方法被子类覆盖/重写时,使用final关键字修饰即可 // A作为父类 class A { // 不希望hi()方法被子类覆盖/重写,使用final关键字修饰即可 public final void hi() {} }
当不希望类的某个属性值被修改,可以用
final
关键字修饰javaclass A { // 不希望某个属性值被修改,使用final关键字修饰即可 public final double TAX_RATE = 0.08; }
当不希望某个局部变量被修改,可以使用
final
关键字修饰javaclass A { public void hi() { // 不希望某个局部变量被修改,使用final关键字修饰即可 final double NUM = 0.01; // 这时NUM也被称为局部常量 NUM = 0.2; // 报错 } }
注意事项:
final
修饰的属性又叫常量,一般用XX_XX_XX
来命名final
修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在下面几个位置:定义时:如:
public final double TAX_RATE = 0.08;
在构造器中
javaclass A { // 在构造器中赋初值 public final double TAX_RATE; public A() { TAX_RATE = 0.08; } }
在代码块中
javaclass A { // 在代码块中赋初值 public final double TAX_RATE; { TAX_RATE = 0.08; } }
如果
final
修饰的属性是静态的,则初始化的位置只能是在定义时和在静态代码块中,不能在构造器中赋值javaclass A { public static final double TAX_RATE1 = 0.02; public static final double TAX_RATE2; static { TAX_RATE2 = 0.08; } }
final
类不能继承,但是可以实例化出具体的对象如果类不是
final
类,但是含有final
方法,则该方法虽然不能重写,但是可以被继承下来供子类进行使用一般来说,如果一个类已经是
final
类了,就没有必要再将方法修饰成final
方法了(因为这个类已经是不能被继承了,就没有子类会去重写这个方法了)final
不能修饰构造方法(即构造器)final
和static
往往搭配使用(两者的前后顺序可以改变),效率更高,底层编译器做了优化处理,不会导致类加载javaclass B { public static int num = 1000; public static int num2 = 2000; static { System.out.println("B类的静态代码块被执行"); } } // 在主函数调用B类中的属性时,会加载类,先执行代码块中的内容 System.out.println(B.num); // B类的静态代码块被执行 1000 // 当final和static一起使用时,不会加载类,不会执行代码块中的内容 System.out.println(B.num2); // 2000
对于类中的方法,其形参是可以用
final
修饰的,但是这样的形参是不能修改的javapublic int add(final int x) { // x的值不能修改 }
对于包装类(
Integer
、Double
、Float
、Boolean
等都是通过final
修饰的),String
也是final
类,这些类都是不能被继承的
抽象类
当父类中的某些方法需要声明时,但是又不确定如何具体的实现,可以将其声明为抽象方法,那么这个类为抽象类
abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
// 这里实现了eat方法,但是实际上没有什么意义,后续在子类中都是要重写的(父类方法不确定性的问题)
// 即将该方法设置为抽象方法
//public void eat() {
// System.out.println("这是一个吃的方法,但是不知道具体吃什么");
//}
public abstract void eat();
}
抽象方法就是没有实现的方法,具体而言,就是没有方法体,这个具体的方法会让子类进行实现(一般来说,抽象类会被继承,由其子类来实现抽象方法)
将方法设置为抽象方法后,必须要将该类设置为抽象类,否则会报错
注意事项:
- 用
abstract
关键字来修饰一个类时,这个类就是一个抽象类 - 抽象类是不能被实例化的
- 抽象类不一定要包含
abstract
方法,也就是说,抽象类可以没有abstract
方法 - 一旦类包含了
abstract
方法,则这个类必须声明为abstract
类 - 抽象类可以有任意成员(因为抽象类的本质还是类),比如:非抽象方法、构造器、静态属性等
- 用
abstract
关键字来修饰一个方法时,这个方法就是一个抽象方法,该方法是没有方法体的,即没有{}
abstract
只能修饰类和方法,不能修饰属性和其他的内容- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法(对父类的抽象方法增加一个方法体
{}
,就表示实现了父类的方法),除非它自己也声明为抽象类 - 抽象类的价值更多的作用是在于设计,是设计者设计好后,让子类继承并实现抽象类
- 抽象方法不能使用
private
、final
和static
来修饰,因为这些关键字都是和重写相违背的
接口
接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,在根据具体情况把这些方法写出来
基本语法:
// 声明接口
interface 接口名 {
// 可以写属性
public int n1 = 10;
// 可以写抽象方法,注意:在接口中,写抽象方法时,可以省略abstract关键字
public void hi();
// jdk8.0后,接口可以使用静态方法
public static void cry() {
System.out.println("cry");
}
// jdk8.0后,接口可以使用默认方法,但是要使用default关键字进行修饰
default public void ok() {
System.out.println("ok");
}
}
// 类去使用接口
class 类名 implements 接口 {
自己属性;
自己方法;
必须实现的接口的抽象方法; // 如果一个类implements接口,需要将该接口的所有抽象方法都实现
}
- 在
jdk7.0
前,接口里的所有方法都没有方法体,即都是抽象方法jdk8.0
后,接口可以有静态方法,默认方法,也就是说接口中可以有方法的具体实现,但是默认方法要使用default
关键字进行修饰
注意事项:
接口不能被实例化
接口中所有的方法都是
public
方法(public
修饰符可以忽略),接口中抽象方法,可以不用abstract
修饰一个普通类实现接口,就必须将该接口的所有方法都实现(重写),可以使用
alt+enter
快捷键来快速实现抽象类实现接口,可以不用实现接口的方法
一个类同时可以实现多个接口,但是也要完成这些接口中的所有抽象方法
接口中的属性,只能是
final
的,而且是public static final
修饰符,比如:int a = 1;
实际上为public static final int a = 1;
(必须初始化)- 因为接口中的属性都是静态的,所以接口中属性的访问形式:接口名.属性名
- 因为接口中的属性是
final
,所以后续我们不可以在外界修改接口中的属性值
接口不能继承其它的类,但是可以继承多个别的接口:
interface A extends B,C {}
声明接口的修饰符只能是
public
和默认,这点和类的修饰符一样
interface A {
int a = 23; // 等价于 public static final int a = 23;
}
class B implements A {}
// 在main方法中
B b = new B();
System.out.println(b.a); // 23 对象实例去访问公共属性
System.out.println(A.a); // 23 接口去访问内部的属性
System.out.println(B.a); // 23 B类实现了A接口,可以使用A接口中的属性
接口的使用场景
我们在具体什么时候需要使用接口,通常有以下的几个场景:
- 以制造飞机为例:对于各种种类的飞机,专家只需要把飞机需要的功能/规格定下来即可,具体的部分让别人实现即可
- 对于一个项目经理来说,管理三个程序员,要开发一个软件,为了控制和管理软件,项目经理可以定义一些接口,然后由程序员具体实现,这样可以更好的管理使用和统一命名规范
实现接口和继承类
实现接口和继承类的区别:Java
提供的实现接口机制,可以理解为对单继承机制的补充
- 继承的价值主要在于:解决代码的复用性和可维护性
- 接口的价值主要在于:设计好各种规范(方法),让其它类去实现这些方法,即更加灵活
- 接口比继承更加灵活,继承是满足
is-a
(是什么)的关系,而接口只需满足like-a
(像什么)的关系 - 接口在一定程度上实现代码的解耦
package com.jlctest.interface;
public class ExtendsVsInterface {
public static void main(String[] args) {
LittleMonkey wukong = new LittleMonkey("climbing");
wukong.climbing(); // wukong猴子会爬树
wukong.swimming(); // wukong通过学习,猴子会游泳
wukong.flying(); // wukong通过学习,猴子会飞翔
}
}
// 鱼类接口
interface Fishable {
void swimming();
}
// 小鸟接口
interface Birdable {
void flying();
}
// 猴子类
class Monkey {
private String name;
public Monkey(String name) {
this.name = name;
}
public void climbing() {
System.out.println(name + "猴子会爬树");
}
public String getName() {
return name;
}
}
// 小猴子类去继承猴子类
class LittleMonkey extends Monkey implements Fishable, Birdable {
public LittleMonkey(String name) {
this.name = name;
}
@Override
public void swimming() {
System.out.println(getName() + "通过学习,猴子会游泳");
}
@Override
public void flying() {
System.out.println(getName() + "通过学习,猴子会飞翔");
}
}
对于继承,只要是继承了这个父类,其子类就可以自动的使用父类中的能力(公共方法);同时继承是单继承的,只能继承一个父类,不同同时继承多个父类
通过实现接口,可以无形的将子类的功能进行加强(如果子类需要拓展功能,可以通过实现接口的方式扩展),
Java
提供的实现接口机制,可以理解为对单继承机制的补充
综合小练习:
interface A {
int x = 0; // 等价于public static final int x = 0;
}
class B {
int x = 1; // 普通属性
}
class C extends B implements A {
public void pX() {
// System.out.println(x); // 错误,原因不明确x到底是接口中的还是类中的
// 访问接口中的x就使用A.x 访问父类的x就使用super.x
System.out.println(A.x);
System.out.println(super.x);
}
}
接口的多态特性
多态参数
接口可以体现多态参数
package com.jlctest.interface;
public class Computer {
// 形参是接口类型 UsbInterface
public void work(UsbInterface usbInterface) {
// 通过接口,来调用方法
usbInterface.start();
}
}
在测试文件中使用work
方法:
package com.jlctest.interface;
public class interfaceTest {
public static void main(String[] args) {
// 创建手机对象,Phone类实现了UsbInterface接口
Phone phone = new Phone();
// 创建计算机对象
Computer computer = new Computer();
// 将手机接入到计算机中,通过多条参数,UsbInterface接口的对象实例是可以传递给work方法的
computer.work(phone); // 只要是实现了UsbInterface接口的对象实例,都可以进行传递
}
}
接口引用可以指向实现了接口的类的对象
package com.jlctest.interface;
public class InterfacePolyParameter {
public static void main(String[] args) {
// 接口的多态体现(与继承体现的多态类似)
// 接口类型的变量 if01 可以指向 实现了IF接口类的对象实例
IF if01 = new Monster();
if01 = new Car();
}
}
// 声明接口
interface IF {}
// 声明两个类去使用接口
class Monster implements IF {}
class Car implements IF {}
多态数组
多态数组指的是接口类型的数组,可以存放实现了这个接口的类的实例
package com.jlctest.interface;
public class InterfacePolyArr {
public static void main(String[] args) {
// 多态数组 -> 接口类型数组
// Usb多态数组,存放Phone和Camera对象
Use[] usbs = new Usb[2]; // 声明多态数组空间
usbs[0] = new Phone();
usbs[1] = new Camera();
// 遍历Usb多态数组,如果是Phone对象,除了调用Usb接口定义的方法体外,还要调用特有的call方法
for(int i = 0; i < usbs.length; i++) {
usbs[i].work(); // 体现了动态绑定机制
// 通过类型的向下转型,进行运行类型的判断,判断运行类型是否为Phone
if(usbs[i] instanceof Phone) {
((Phone) usbs[i]).call();
}
}
}
}
// 声明接口
interface Usb {
void work();
}
// 声明两个类去使用接口
class Phone implements Usb {
@Override
public void work() {
System.out.println("手机工作中...");
}
public void call() {
System.out.println("手机可以打电话...");
}
}
class Camera implements Usb {
@Override
public void work() {
System.out.println("相机工作中...");
}
}
多态传递
package com.jlctest.interface;
public class InterfacePolyPass {
public static void main(String[] args) {
// 接口类型的变量 ig 可以指向 实现了该接口类的对象实例
IG ig = new Teacher();
// 但是 IH ih = new Teacher();会报错,Teacher类没有实现IH接口 interface IG {}
// 但是如果让IG接口去继承IH接口,进行多态传递 interface IG extends IH{}
IH ih = new Teacher(); // 不报错
}
}
// 声明接口
interface IH {}
// interface IG {}
interface IG extends IH{}
// 声明类去使用接口
class Teacher implements IG {}
具体来说:如果IG
继承了IH
接口,而Teacher
类实现了IG
接口,那么,实际上就相当于Teacher
类也实现了IH
接口
内部类
内部类是指,一个类的内部又完整的嵌套了另一个类结构。被嵌套的类被称为内部类(inner class
),嵌套其他类的类称为外部类(outer class
)
基本语法:
class Outer { // 外部类
class Inner { // 内部类
...
}
}
class Other { // 外部其他类
...
}
内部类是类的第五大成员,内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系,内部类是重点也是难点,底层源码使用了大量的内部类
class Outer { // 外部类
// 属性
private int n1 = 100;
// 方法
public void m1() {
System.out.println("m1()");
}
// 构造器
public Outer(int n1) {
this.n1 = n1;
}
// 代码块
{
System.out.println("代码块");
}
// 内部类
class Inner {
...
}
}
内部类有四种,内部类的分类有两种:
- 定义在外部类局部位置上(如方法体内):
- 局部内部类(有类名)
- 匿名内部类(没有类名,是一个重点)
- 定义在外部类的成员位置上(属性和方法位置上):
- 成员内部类(没有
static
修饰) - 静态内部类(使用
static
修饰)
- 成员内部类(没有
局部内部类
局部内部类是定义在外部类的局部位置,通常在方法中,并且有类名
基本语法:
class Outer { // 外部类
// 属性
private int n1 = 100;
// 方法
public void m1() {
// 定义局部内部类
class Inner {
// 可以直接访问外部类的所有成员,包含私有的(私有属性只能在本类进行访问)
public void f1() {
// 局部内部类访问外部类的成员方式是直接访问
System.out.println(n1); // 100
}
}
// 同一作用域中,在外部类方法中,访问局部内部类的成员
// 先创建局部内部类对象,再通过对象进行访问
Inner inner = new Inner();
inner.f1();
}
}
注意事项:
局部内部类,其本质还是一个类,类的要素都有
局部内部类可以直接访问外部类的所有成员,包含私有的
不能给局部内部类添加访问修饰符,因为它的地位就是一个局部变量,局部变量是不能使用修饰符的,但是可以使用
final
修饰,因为局部变量也可以使用final
局部内部类的作用域:仅仅在定义它的方法体或代码块中
局部内部类访问外部类的成员方式:直接访问(局部内部类-->访问-->外部类的成员)
外部类访问局部内部类的成员方式:先创建对象再访问,同时必须要在作用域内
外部其他类是不能访问局部内部类的(因为局部内部类的地位是一个局部变量)
如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(
外部类名.this.成员
)去访问javaclass Outer { private int n1 = 100; public void m1() { // 定义局部内部类 class Inner { private int n1 = 200; public void f1() { System.out.println(n1); // 200 重名时,就近访问 // 如果一定要访问外部类,可以使用外部类名.this.成员的方式 // Outer.this本质为外部类的一个实例对象,即外界主函数中哪个对象调用了m1()方法,Outer.this就指向哪个对象 System.out.println(Outer.this.n1); // 100 } } } }
匿名内部类
匿名内部类在底层的框架和项目的开发中使用的非常多
匿名内部类是定义在外部类的局部位置,比如方法中,并且没有类名
基本语法:
new 类或接口(参数列表) {
类体;
};
基于接口的匿名内部类
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}
class Outer { // 外部类
private int n1 = 10;
public void method() {
// 传统方式是写一个类,实现该接口,再创建这个对象,并指向这个接口
// 如果需求是这个类只使用一次,那么定义的Tiger其他外部类就会浪费
IA tiger = new Tiger();
tiger.cry(); // 老虎叫...
// 针对上述的情况,我们可以使用接口的匿名内部类进行简化,将外界的Tiger其他外部类去掉
// tiger的编译类型是IA 运行类型是匿名内部类,底层为class XXX implements IA { ... }
// 匿名内部类在底层中系统会分配一个类名的,XXX在底层为Outer$1 (外部类+$+数字)
// jdk底层在创建匿名内部类Outer$1,立即就创建了Outer$1的实例,并且将地址返回给tiger
// 匿名内部类使用一次,就不能再使用了,不是tiger对象,tiger对象可以一直使用
IA tiger = new IA() {
@Override
public void cry() {
System.out.println("老虎叫..."):
}
};
tiger.cry(); // 老虎叫...
}
}
interface IA { // 接口
public void cry();
}
class Tiger implements IA {
@Override
public void cry() {
System.out.println("老虎叫..."):
}
}
基于类的匿名内部类
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}
class Outer { // 外部类
private int n1 = 10;
public void method() {
// 创建一个Father的实例 编译类型和运行类似都是Father
Father father = new Father("jack");
// 基于类的匿名内部类
// father的编译类型是Father 运行类型是匿名内部类(外部类+$+数字)
// 运行类型是匿名内部类,底层为class XXX extends Father { ... } 有个继承的关系
// 使用匿名内部类,同时也会返回匿名内部类的对象,将对象地址返回给father
// 这里的参数列表,会传递给Father类写好的构造器
Father father = new Father("jack") {
@Override
public void test() {
System.out.println("匿名内部类重写了test方法");
// 可以直接访问外部类的所有成员,包含私有的
System.out.println(n1); // 10
}
}
fatehe.test(); // 匿名内部类重写了test方法
// 基于抽象类的匿名内部类
Animal animal = new Animal() {
// 抽象类中下面内容是必须要写的
@Override
void eat() {
System.out.println("小狗吃骨头");
}
}
animal.eat(); // 小狗吃骨头
}
}
class Father {
// 构造器
public Father(String name) {}
public void test() {}
}
abstract class Animal { // 抽象类
abstract void eat();
}
注意事项:
匿名内部类的本质也是一个类,是一个内部类(即定义在外部类的局部位置,比如方法中),且是没有类名的,同时匿名内部类还是一个对象
匿名内部类既是一个类的定义,同时它本身也是一个对象,从语法上看,它既有定义类的特征,也有创建对象的特征
java// 可以直接调用匿名内部类的方法 本质上就是对象.方法 匿名内部类本身也是返回对象 new Father("jack") { @Override public void test() { System.out.println("匿名内部类重写了test方法"); } }.test(); // 匿名内部类重写了test方法
匿名内部类可以直接访问外部类的所有成员,包含私有的
匿名内部类不能添加访问修饰符,因为它的地位就是一个局部变量
匿名内部类的作用域:仅仅在定义它的方法或代码块中
外部其他类不能访问匿名内部类(因为匿名内部类的地位是一个局部变量)
如果外部类和匿名内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(
外部类名.this.成员
)去访问
匿名内部类的最佳实践
当做实参直接传递,简洁高效
public class AnonymousInnerClass {
public static void main(String[] args) {
// 使用匿名内部类当做实参直接传递,简洁高效
f1(new IL() {
@Override
public void show() {
System.out.println("这是一幅名画");
}
});
}
// 定义静态方法,形参是接口类型
public static void f1(IL il) {
il.show();
}
}
interface IL { // 接口
void show();
}
如果不使用匿名内部类,我们就需要写出这个类去实现这个接口,通过硬编码的方式去实现
在匿名内部类中修改内容,只是影响这一个实参传入,如果使用硬编码的方式,修改了类中的内容,就会修改所有基于这个类实例出的对象
成员内部类
成员内部类是定义在外部类的成员位置,并且没有static
修饰
class Outer { // 外部类
private int n1 = 10;
// 定义成员内部类
class Inner {
public void say() {
// 成员内部类中可以直接访问外部类中的所有成员
System.out.println(n1); // 10
}
}
// 访问成员内部类中的方法,通过成员内部类实例化出一个对象,再使用这个对象进行调用
public void t1() {
Inner inner = new Inner();
inner.say();
}
}
注意事项:
成员内部类可以直接访问外部类的所有成员,包括私有的
成员内部类可以添加任意访问修饰符(
public
、protected
、默认、private
),因为成员内部类的地位就是一个成员成员内部类的作用域和外部类的其他成员一样,为整个类体(成员内部类的类名可以在整个外部类中使用)
外界类去访问成员内部类中的属性和方法,需要创建对象再调用
外部其他类也可以访问成员内部类,有两种常见的方式:
javapublic class AnonymousInnerClass { public static void main(String[] args) { Outer outer = new Outer(); // 第一种方式 // 相当于将new Inner()当作outer对象的一个成员 Outer.Inner inner01 = outer.new Inner(); inner01.say(); // 第二种方式 // 在外部类中,编写一个方法,可以返回Inner对象 Outer.Inner inner02 = outer.getInnerInstance(); inner02.say(); } } class Outer { // 外部类 private int n1 = 10; // 定义成员内部类 class Inner { public void say() { // 成员内部类中可以直接访问外部类中的所有成员 System.out.println(n1); // 10 } } // 为外界返回一个Inner对象实例,供方法二调用 public Inner getInnerInstance() { return new Inner(); } }
如果外部类和成员内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(
外部类名.this.成员
)去访问
静态内部类
静态内部类是定义在外部类的成员位置,并且有static
修饰
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
// 外部其他类访问静态内部类
// 第一种方式
// 静态内部类是可以通过类名直接访问的(前提是满足访问权限,私有的是不能访问的)
Outer.Inner inner01 = new Outer.Inner();
inner01.say();
// 第二种方式
// 在外部类中,编写一个方法,可以返回Inner对象实例
Outer.Inner inner02 = outer.getInnerInstance();
inner02.say();
// 通过静态方法返回 静态方法是可以直接通过类去调用的
Outer.Inner inner03 = Outer.getInner();
inner03.say();
}
}
class Outer { // 外部类
private int n1 = 10;
private static String name = "jlc";
// 定义静态内部类
static class Inner {
public void say() {
// 静态内部类可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
System.out.println(n1); // 报错
System.out.println(name); // jlc
}
}
// 静态内部类的作用域:同其他的成员,为整个类体
public void m1() {
Inner inner = new Inner();
inner.say();
}
// 为外界返回一个Inner对象实例,供方法二调用,通过非静态的方法
public Inner getInnerInstance() {
return new Inner();
}
// 也可以通过静态的方法,返回Inner对象实例
public static Inner getInner() {
return new Inner();
}
}
注意事项:
- 静态内部类可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
- 可以添加任意访问修饰符(
public
、protected
、默认、private
),因为静态内部类的地位就是一个成员 - 静态内部类的作用域:同其他的成员,为整个类体
- 外部类访问静态内部类的成员,需要先创建对象,在调用访问
- 如果外部类和静态内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(
外部类名.this.成员
)去访问