1. 类和对象
1.1. 面向对象编程
public class Person {
String name;
int age;
public void eat() {
System.out.println("eat");
}
public void work() {
System.out.println("work");
}
public void rest(String site) {
System.out.println(name + "在" + site + "休息");
}
}
public class Test {
public static void main(String[] args) {
int n;
n = 5;
System.out.println(n);
Person person1;
person1 = new Person();
person1.age = 23;
person1.name = "zhangsan";
person1.eat();
person1.work();
person1.rest("宿舍");
Person person2 = new Person();
person2.name = "lisi";
person1.age = 24;
person2.work();
person2.eat();
person2.rest("豪宅");
}
}
内存分布图:
1.2. 局部变量
类中定义的变量是成员变量,而方法中定义的变量,包括方法的参数,代码中定义的变量被称为局部变量
1.3. 构造方法
对于一个类来说,有三种常见的成员:属性、方法和构造器,这三种成员都可以定义零个或多个
构造器:用于对象的初始化,是一个创建对象时被自动调用的特殊方法。构造器的名称应与类名一致。Java通过new关键字来调用构造器,从而返回该类的实例
声明格式:
[修饰符] 类名(形参列表) {
// n条语句
}
1. 构造器的方法名必须和类名一致
2. 构造器通过new关键字调用
3. 构造器不能定义返回值类型(返回值类型肯定是本类),不能在构造器里使用return
4. 如果没有定义构造器,则编译器会自动定义一个无参的构造方法。
构造方法也是方法,与普通的方法一样,也可以重载
public class Computer {
private String cpu = "Intel";
private String memory;
private String mainBoard;
private String keyBoard;
public Computer() {
System.out.println("---computer---");
cpu = "AMD";
}
public Computer(String cpu, String memory, String mainBoard, String keyBoard) {
this.cpu = cpu;
this.memory = memory;
this.mainBoard = mainBoard;
this.keyBoard = keyBoard;
}
public void start() {
System.out.println("---start---");
}
public void close() {
System.out.println("---close---");
}
public void show() {
System.out.println("cpu=" + cpu + " memory=" + memory + " mainBoard=" + mainBoard + " keyBoard=" + keyBoard);
}
public static void main(String[] args) {
Computer computer1 = new Computer();
computer1.cpu = "酷睿";
computer1.memory = "三星";
computer1.keyBoard = "罗技";
computer1.mainBoard = "华硕";
computer1.start();
computer1.show(); // cpu=AMD memory=三星 mainBoard=华硕 keyBoard=罗技
computer1.close();
Computer computer = new Computer("酷睿", "三星", "华硕", "罗技");
computer.start();
computer.show(); // 同上
computer.close();
}
}
易错问题:
Q:对象的创建完全是由构造方法实现的吗?
A:不完全是,构造方法通过new关键字调用构造器时,构造器也返回了该类对象,但这个对象并不是完全由构造器负责创建的。创建一个对象的四步:
1. 分配对象空间,并将对象成员变量初始化为0或空
2. 执行属性值的显示初始化
3. 执行构造方法
4. 返回对象的地址给相关的变量
注意:如果方法构造中形参名与属性名相同,需要使用this关键字区分属性与形参。this.id表示属性id,id表示形参id
1.4. 对象数组
public class TestArray {
public static void main(String[] args) {
int[] arr;
arr = new int[4];
arr[0] = 90;
arr[1] = 80;
arr[2] = 100;
arr[3] = 54;
for (int i : arr) {
System.out.println(i);
}
System.out.println("===========");
Computer[] arr2 = new Computer[4];
arr2[0] = new Computer("酷睿", "三星", "华硕", "罗技");
arr2[1] = new Computer("Inter", "金士顿", "华硕", "罗技");
arr2[2] = new Computer("AMD", "三星", "华硕", "罗技");
arr2[3] = new Computer("酷睿", "金士顿", "华硕", "罗技");
for (Computer computer:arr2) {
System.out.println(computer.toString());
computer.show();
}
}
}
内存分配图:
2. 构造方法
2.1. 方法调用
2.1.1. 基本数据类型方法调用
public class TestPrimaryArgs {
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
System.out.println("交换前:num1: " + num1 + ",num2:" + num2); // 交换前:num1: 10,num2:20
swap(num1, num2);
System.out.println("交换后:num1: " + num1 + ",num2: " + num2); // 交换后:num1: 10,num2: 20
}
public static void swap(int num1, int num2) {
int temp = num1;
num1 = num2;
num2 = temp;
System.out.println("交换后inner:num1: " + num1 + ",num2: " + num2); // 交换后inner:num1: 20,num2: 10
}
}
2.1.2. 引用数据类型方法调用
public class Point {
int x;
int y;
}
public class TestRefArgs {
public static void main(String[] args) {
Point p = new Point();
p.x = 10;
p.y = 20;
System.out.println("交换前:p.x=" + p.x + ",p.y=" + p.y); // 交换前:p.x=10,p.y=20
swap(p);
System.out.println("交换后:p.x=" + p.x + ",p.y=" + p.y); // 交换后:p.x=20,p.y=10
}
public static void swap(Point p) {
int temp = p.x;
p.x = p.y;
p.y = temp;
}
}
2.1.3. 总结
基本数据类型的参数是值传递,引用数据类型的参数是传递的是引用(地址),本质上也是值传递。
2.2. this
创建一个对象分为四步:
- 分配对象空间,并将对象成员变量初始化为0或空
- 执行属性值的显示初始化
- 执行构造方法
- 返回对象的地址给相关的变量
this的本质就是创建好的对象的地址,由于在构造方法调用前,对象已经创建。因此,在构造方法中this可以代表当前对象
this的常用用法:
- 调用成员变量:如果成员变量和局部变量同名,this用来区分两者;如果没有同名的局部变量,this可以不写
- 调用成员方法:this可省
- 调用构造方法:使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句
- this不能用于static方法中
public class Student {
private int sno;
private String name;
private String sex;
private double score;
public Student() {}
public Student(int sno, String name, String sex) {
this.sno = sno;
this.name = name;
this.sex = sex;
}
public Student (int sno, String name, String sex, double score) {
this(sno, name, sex);
this.score = score;
}
private void study() {
this.shout(); // 1. 好好学习,天天向上
shout(); // 2. 好好学习,天天向上
System.out.println("努力学习"); // 3. 努力学习
}
public void shout() {
System.out.println("好好学习,天天向上"); // 4. 好好学习,天天向上
}
public void show() {
System.out.println(sno+"一" + this.name + "一" + sex + "一" + this.score); // 5. 7一wyb一男一27.0
}
public static void main(String[] args) {
Student stu = new Student(7, "wyb", "男", 27);
stu.study();
stu.shout();
stu.show();
Student stu2 = new Student();
}
}
3. static关键字
一个类的成员包括变量、方法、构造方法、代码块和内部类,static可以修饰除了构造方法以外的所有成员。
使用static修饰的成员成为静态成员,是属于某个类的;而不使用static修饰的成员是实例成员,是属于类的每个对象的
3.1. static变量
在类中,用static声明的成员变量为静态成员变量,也称类变量。静态成员变量的生命周期和类相同,在整个应用程序执行期间都有效。特点如下:
- 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时显式初始化
- 对于该类的所有对象来说,static成员变量只有一份,被该类的所有对象共享
- 一般用"类名.类属性/方法"来调用。(也可以通过对象引用或类名(不需要视力化)访问静态成员)
- 在static方法中不可直接访问非static的成员
public class Student {
private String cup;
private static String classRoom;
public Student() {}
public Student(String cup) {
this.cup = cup;
}
public void show() {
System.out.println(cup + "一" + classRoom); // 迪士尼一null
}
public static void main(String[] args) {
Student stu = new Student("迪士尼");
stu.show();
System.out.println(Student.classRoom); // null
Student.classRoom = "332";
System.out.println(Student.classRoom); // 332
}
}
总结:static变量和非static变量的区别:
1. 份数不同:静态变量1份;非静态变量,一个对象一份
2. 存储位置不同:静态变量存储在方法区;非静态变量存储在堆中
3. 内存分配空间的时间不同:静态变量在第一次加载类的时候分配空间;非静态变量在创建对象时分配空间
4. 生命周期不同:静态变量和类的生命周期相同;非静态变量的生命周期和所属对象相同
5. 调用方式不同:静态变量:通过类名调用 Student.classRomm;也可以通过对象名调用stu.classRoom = “332”,不推荐;非静态变量:通过对象名调用 stu.name = “wyb”
3.2. static方法
- static方法的作用:访问static变量和static方法
- static方法的调用方式:通过类名调用:Student.showClassRoom(),推荐该方式;通过对象名访问:stu.showClassRoom();
- 静态方法中不可以访问非静态变量、非静态方法和this;理解:加载类的时候就加载静态变量和静态方法,此时可能还没创建对象,所以非静态变量和非静态方法还没有分配空间,无法访问
- 非静态方法中可以访问静态变量、静态方法;理解:加载类时就已经加载静态变量和静态方法了,创建对象后,非静态变量和非静态方法才分配空间,此时静态变量和静态方法已经存在,可以访问
public class Student2 {
private String cup;
private static String classRoom;
public static void showClassRoom() {
System.out.println(classRoom);
/*System.out.println(cup); // 非静态变量,不可访问
show(); // 非静态方法,不可访问
System.out.println(this); // this,不可访问*/
}
public static void setClassRoom(String classRoom) {
Student2.classRoom = classRoom;
}
public Student2() {}
public Student2(String cup) {
this.cup = cup;
}
public void show() {
System.out.println(cup + "一" + classRoom); // 3. 迪士尼一614
showClassRoom(); // 4. 614
}
public static void main(String[] args) {
Student2.showClassRoom(); // 1. null
Student2.setClassRoom("504");
Student2.showClassRoom(); // 2.504
Student2 stu = new Student2("迪士尼");
stu.classRoom = "614";
stu.show();
stu.showClassRoom(); // 5. 614
}
}
3.3. static代码块
- 局部代码块:
- 位置:方法中
- 数量:多个
- 执行顺序:依次执行
- 局部代码块中定义的变量作用范围只限于当前代码块
- (成员)代码块
- 位置:类中
- 数量:多个
- 执行顺序:依次执行
- 执行时间:每次创建对象时都执行;先执行代码块,再执行构造方法
- 作用:实际开发中很少用;可以将各个构造方法中公共的代码提取到代码块;匿名内部类不能提供构造方法,此时初始化操作放到代码块中
- static代码块
- 位置:类中
- 数量:多个
- 执行顺序:依次执行
- 执行时间:第一次加载类时执行,只执行一次
- 作用:给静态变量赋初始值。实际开发中使用较多,一般用于执行一些全局性的初始化操作,比如创建工厂、加载数据库初始信息
public class Student3 {
static {
System.out.println("static code block1"); // 1. static code block1
}
String name;
static String classRoom;
static {
// name = "wyb";
classRoom = "332";
System.out.println("static code block2"); // 2. static code block2
}
public Student3() {
System.out.println("-------Student3-------"); // 6. -------Student3-------
}
public static void main(String[] args) {
int n = 4; // 局部变量
// 局部代码块
{
int m = 5;
System.out.println(m); // 3. 5
}
System.out.println(classRoom); // 4. 332
new Student3();
}
{
classRoom = "504";
System.out.println("code block3"); // 5. code block3
}
}
4. package和import
4.1. package包
包机制时java中管理类的重要手段。开发中,我们会遇到大量同名的类,通过包可以解决类重名的问题,也可以实现对类的有效管理。还可以和访问权限有密切关系。
- 定义包
- 包名:域名倒着写,再加上模块,便于内部管理类
- 包名一律小写
- 使用包:通常是类第一句非注释性语句,必须以;结尾
- java常用包
注意:- 写项目时都要加包,不要使用默认包
- com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。
4.2. import导入
使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。
注意:
- 默认是当前包的类和接口
- java会默认导入java.lang包下的所有类,因此这些类可以直接使用
- 可以使用通配符,比如import com.baidu.oop.object.*;会导入该包下所有类和接口(但不包括下级包)
- 如果导入两个同名的类,只能用包名+类名来显示调用相关类:java.util.Date date = new java.util.Date();
使用静态导入static import:可以导入指定类的静态属性和静态方法
import java.lang.reflect.Constructor;
import java.util.Date;
import java.util.*;
import static java.lang.Math.PI;
import static java.lang.Math.*;
public class TestImport {
public static void main(String[] args) {
Date date = new Date();
java.sql.Date date1 = java.sql.Date.valueOf("1991-10-06");
List list;
Set set;
Map map;
Constructor constructor;
System.out.println(PI);
System.out.println(sqrt(64));
System.out.println(pow(7,2));
}
}
5. 封装
封装是面向对象三大特征之一。对于程序合理的封装让外部调用更加方便,更加利于协作,同时,同于实现者来说也更加容易修正和改版代码
5.1. 封装优点
- 提高代码的安全性
- 提高代码的复用性
- 高内聚:封装细节,便于修改内部代码,提高可维护性
- 低耦合:简化外部调用,便于调用者使用,便于扩展和维护
5.2. 权限修饰符
使用权限修饰符可以尽可能的让访问权限降到最低,从而提高安全性。
- private表示私有,只有自己类能访问
- default表示没有修饰符修饰,只有同一个包的类能访问
- protected表示可以被同一包的类以及其他包中的子类访问
- public表示可以被该项目的所有包中的所有类访问
- 类成员的处理:
- 一般使用private访问权限
- 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头的)
- 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰
- 类的处理:
- 类只能使用public和默认来修饰
- 默认:当前包
- public:当前项目的所有包
- public类要求类名和文件名相同,一个java文件中至多一个public类
public class Person {
private String name;
private int age;
public Person() {}
public Person(String name, int age) {
this.name = name;
setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
if(age > 130 || age < 0) {
this.age = 18;
}else {
this.age =age;
}
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person [name=" + getName() + ", age=" + getAge() + "]";
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person();
p1.setName("wyb");
p1.setAge(27);
System.out.println(p1); // Person [name=wyb, age=27]
Person p2 = new Person("xz", 32);
System.out.println(p2); // Person [name=xz, age=32]
}
}
6. 继承
继承是面向对象编程三大特征之一,可以更加容易实现对已有类的扩展和对现实世界的建模
6.1. 继承及其作用
继承更加容易实现了类的扩展。实现了代码的重用
public class Animal {
private String color;
private int age;
public Animal() {
super();
}
public Animal(String color, int age) {
this.color = color;
this.age = age;
}
public void eat() {
System.out.println("eat");
}
public void show() {
System.out.println(color + " " + age);
}
}
public class Dog extends Animal {
private String nickName;
public Dog() {
}
public Dog(String color, int age) {
}
public Dog(String color, int age, String nickName) {
super(color, age);
this.nickName = nickName;
}
public void guard() {
System.out.println("wang wang");
}
}
public class Cat extends Animal{
private int eyeSight;
public Cat() {
super();
}
public Cat(String color, int age, int eyeSight) {
super(color, age);
this.eyeSight = eyeSight;
}
public void grabMouse() {
System.out.println("vat grad mouse");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("黑色",3, "大黄");
dog.guard(); // wang wang
dog.show(); // 黑色 3
Cat cat = new Cat("小花",12, 5);
cat.grabMouse(); // vat grad mouse
cat.show(); // 小花 12
}
}
- 继承的使用要点:
- 父类也称超类、基类,子类称为派生类
- java中只有单继承,没有多继承,多继承会引起混乱,使得继承链过于复杂,系统难以维护
- 子类继承父类,可以得到父类的全部属性和方法(父类的构造方法除外),但不一定可以直接访问(父类的私有属性和方法不可以访问)
- 如果定义一个类时,没有调用extends,则他的父类是:java.lang.Object
6.2. 方法重写
父类的方法无法满足子类的需求,可通过方法重写(override)解决
@Override
public String toString() {
// return super.toString();
return "Animal[color=" + color + ", age=" + age + "]";
}
6.3. 总结
方法重载overload和重写 override是面向对象的两个重要概念,下面是他们之间的区分:
- 总体区别:
- 重载:位于同一个类中,在一个类里为一种行为提供多种实现方式并提高可读性
- 重写:位于子类和父类间,父类方法无法满足子类的要求,子类通过方法重写满足要求
- 细节区别:
- 重载:与修饰符,返回值,抛出异常无关,方法名要相同,参数不同
- 重写:子类修饰符>=父类,子类返回值<=父类,方法名,参数相同,子类抛出异常<=父类
重载实例:构造方法重载、println()方法重载
重写实例:Object类的toString()、equals()、hashCode()等都可以被子类重写
注意:
- 某些方法使用final修饰,将无法被重写。比如Object类的wait()、notify()等
- 静态方法无法进行方法重写。如果父类中含有一个静态方法,且在子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是将父类中的该同名方法进行了隐藏,而非重写,添加@override注解会报错。
7. 继承
7.1. Object类
Object类是所有java类的根基类,所有的java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明父类,则默认继承Object类
面试题:写出Object类的6个方法:
-
Object的public方法
- equals(Object obj):指示其他某个对象是否与此对象相等,返回boolean
- getClass():返回此Object的运行时类
- hashCode():返回该对象的哈希码值
- notify():唤醒在此对象监视器上等待的单个线程
- notifyAll():唤醒在此对象监视器上等待的所有线程
- toString():返回该对象的字符串表示
- wait():在其他线程调用此对象的notify()方法或notifyAll()方法前,导致当前线程等待
- wait(long timeout):在其他线程调用此对象的notify()方法或notifyAll()方法,或其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待
-
Object的private、protected方法
- clone():创建并返回此对象的一个副本
- finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
-
native关键字:
- 一个native方法就是一个java调用非java代码的接口,该方法不是由java实现的,而是由c或c++实现的
- 定义一个native方法,并不提供实现体。
7.2. 成员变量的隐藏
如果父类和子类中有同名的成员变量,不存在变量的重写,分别占有自己的空间。子类的成员变量优先,称为成员变量的隐藏。
public class Animal {
String color = "Animal的color";
int age;
public String getColor() {
return color;
}
}
public class Dog extends Animal {
String color = "Dog的color";
String nickName;
public String getColor() {
return color;
}
public String getSuperColor() {
return super.getColor();
}
public void show() {
String color = "方法的color";
System.out.println(color); // 方法的color
System.out.println(this.color); // Dog的color
System.out.println(super.color); // Animal的color
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.show();
System.out.println(dog.getColor()); // Dog的color
System.out.println(dog.getSuperColor()); // Animal的color
}
}
7.3. 继承情况下构造方法的调用过程
- 继承条件下构造方法的执行顺序
- 构造方法的第一条语句默认是super(),含义是调用父类无参数的构造方法
- 构造方法的第一条语句可以显式的指定为父类的有参数构造方法:super(…)
- 构造方法的第一条语句可以显式的指定为当前类的构造方法:this(…)
- 注意
- 每个类最好要提供无参数的构造方法
- 构造方法的第一条语句可以是通过super或this调用构造方法,须是第一条语句
- 构造方法中不能同时使用super和this调用构造方法,并不是说不能同时出现this和super
public class Animal1 {
String color;
private int age;
public Animal1() {
super();
}
public Animal1(String color, int age) {
this.color = color;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Dog1 extends Animal1{
private String nickName;
private String type;
public Dog1() {
super();
}
public Dog1(String color,int age, String nickName) {
super(color, age);
this.nickName = nickName;
}
public Dog1(String color,int age, String nickName, String type) {
this(color, age, nickName);
this.type = type;
}
public String toString() {
return this.color + " " + this.getAge() + " " + this.nickName + " " + this.type;
}
public static void main(String[] args) {
Dog1 dog1 = new Dog1("黑色",3,"旺财","泰迪");
System.out.println(dog1.toString()); // 黑色 3 旺财 泰迪
}
}
7.4. super关键字
super可以看作是直接父类对象的引用。每一个子类对象都会有一个super引用指向其直接父类对象
- 使用super可以:
- 调用成员变量:super.color
- 调用成员方法:super.introduce();
- 调用构造方法:super(color, age);
- 注意:
- 使用super调用普通方法,语句没有位置限制,可以在子类中随便调用
- 在一个类中,若是构造方法的第一行代码没有显示的调用super(…)或者this(…);那么java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super可以省略
7.5. ==和equals方法
==:代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等
equals:默认比较两个对象的hashcode,是同一个对象的引用时返回true,否则返回false。显然,这无法满足子类的要求,可根据要求重写equals方法
重写equals方法:
public class Animal2 {
String color;
private int age;
public boolean equals(Object obj) {
Animal2 other = (Animal2) obj;
if(obj == null) return false;
// 如果两个变量指向同一个空间,直接返回true
if(this == obj) return true;
if(this.color.equals(other.color) && this.age == other.age) {
return true;
}else {
return false;
}
}
}
public class Dog2 extends Animal2{
private String nickName;
private String type;
public boolean equals(Object obj) {
Dog2 other = (Dog2) obj;
boolean flag = super.equals(obj);
if(!flag) {
return false;
}else {
// 如果Animal2的equals返回true,需要再比较nickName、type
if(this.nickName.equals(other.nickName) && this.type.equals(other.type)) {
return true;
}else {
return false;
}
}
}
}
7.6. 组合
继承和组合是复用代码的两种方式
- 继承:is-a,Dog is a Animal, Cat is a Animal
- 组合:has-a,Computer has a cup memery mainBoard
面向对象的设计原则之一:组合聚合复用原则(优先使用组合,而不是继承)
- 除非两个类之间是is-a的关系,否则不要轻易使用继承;过多的继承会破坏代码的可维护性,因为父类被修改会影响到所有继承自他的子类
- 如果类之间没有is-a的关系,可以通过实现接口与组合的方式来达到相同的目的。采用接口与组合的方式具有更好的扩展性
public class Cpu {
private String name;
private double rate;
public void calc() {
System.out.println("cpu calc...");
}
public void control() {
System.out.println("cpu control...");
}
}
public class AMDCpu extends Cpu{
public void calc() {
System.out.println("AMD calc");
}
}
public class IntelCpu extends Cpu{
public void calc() {
System.out.println("IntelCpu calc");
}
}
public class MainBoard {
public void connect() {
System.out.println("main board connected");
}
}
public class Memory {
public void process() {
System.out.println("memory process");
}
}
public class Computer {
private Cpu cpu = new Cpu();
private Memory memory = new Memory();
private MainBoard mainBoard = new MainBoard();
private void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public void computer() {
cpu.calc(); // AMD calc
cpu.control(); // cpu control...
memory.process(); // memory process
mainBoard.connect(); // main board connected
}
public static void main(String[] args) {
Computer computer = new Computer();
Cpu cpu = new AMDCpu();
Cpu cpu1= new IntelCpu();
Cpu cpu2= new Cpu();
computer.setCpu(cpu);
computer.computer();
}
}
8. 多态
多态是面向对象三个特征之一。同一行为,通过不同的子类,可以体现出来的不同的形态
8.1. 引入和使用多态
多态指的是同一个方法调用,由于对象不同可能会有不同的行为。
- 编译期类型:=左边的类型
- 运行期类型:=右边的类型
当有继承关系时,可能发生编译期类型和运行期类型不同的情况,即编译期类型是父类类型,运行期类型是子类类型:父类引用指向子类对象
- 多态的要点
- 多态是方法的多态,不是属性的多态(多态与属性无关)
- 多态存在的3个必要条件:继承,方法重写,父类引用指向子类对象
- 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了
public class Programmer {
public String name = "proName";
// 1. 子类继承的
public void writeCode() {
System.out.println("writing code...");
}
// 2. 子类重写的
public void eat() {
System.out.println("eat...");
}
}
public class Chinese extends Programmer{
public String name = "ChinaName";
// 1. 从父类继承一个方法writeCode();
// 2. 重写父类的一个方法eat
public void eat() {
System.out.println("chinese eat");
}
// 3. 子类特有的方法
public void playShadowBoxing() {
System.out.println("chinese playShadowBoxing");
}
}
public class English extends Programmer{
// 1. 从父类继承一个方法writeCode
// 2. 重写父类的方法eat
public void eat() {
System.out.println("english eat");
}
// 3. 子类特有的方法
public void raceHorse() {
System.out.println("english race");
}
}
public class Test {
public static void showEat(Programmer pro) {
pro.eat();
}
public static void main(String[] args) {
Programmer ch = new Chinese();
showEat(ch); // chinese eat
English en = new English();
showEat(en); // english eat
Programmer pro = new Programmer();
showEat(pro); // eat...
}
}
public class Test1 {
public static void main(String[] args) {
Chinese ch = new Chinese();
ch.writeCode(); // writing code...
ch.eat(); // chinese eat
ch.playShadowBoxing(); // chinese playShadowBoxing
Programmer pro = new Programmer();
pro.writeCode(); // writing code...
pro.eat(); // eat...
}
}
使用父类做方法的行参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,符和开闭原则。
父类引用做方法的行参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。即使增加了新的子类,方法也无需改变,提高了扩展性,符和开闭原则。
- 面线对象的设计原则之一:开闭原则OCP
- 对扩展开放,对修改关闭
- 软件系统中的各种组件应该在不修改现有代码的基础上,引入新功能
8.2. 多态之向上转型
将子类对象赋给父类引用,称为向上转型,自动进行类型转换。向上转型可以调用子类继承的方法,但不能调用子类特有的方法。如果子类重写了父类的方法,向上转型后通过父类引用调用的是真实子类重写的方法。
public class TestPoly {
public static void main(String[] args) {
// 基本数据类型的自动转换
int n = 10;
System.out.println(n); // 10
// 左 > 右 自动转换
double d = n;
System.out.println(d); // 10.0
// 引用数据类型自动转换
Programmer programmer = new Chinese(); // 自动转换,向上转型 左 > 右
programmer.writeCode(); // writing code...
programmer.eat(); // chinese eat
}
}
8.3. 向下转型
将父类的引用变量转换为子类类型,称为向下转型。向下转型后就可以调用子类特有的方法了
- 需要进行强制转换:Chinese ch = (Chinese)pro;
- 强制转换必须转换成真实子类型,否则报错ClassCaseException
- 向下转型之间肯定发生了向上转型
- 为了避免ClassCaseException,向下转型之前使用instanceof先判断一下
- pro instanceof Chinese 对象 instanceof 类/接口
- 使用instanceof的前提:左边的对象和右边的类型在继承树上有上下级关系
public class TestPoly1 {
public static void main(String[] args) {
double d = 3.14;
System.out.println(d); // 3.14
int n = (int)d;
System.out.println(n); // 3
Programmer programmer = new English();
// 多态
programmer.eat(); // chinese eat
// 左 < 右,必须转换成原来的真实子类型
// Chinese ch = (Chinese) programmer;
// ch.playShadowBoxing(); // java.lang.ClassCastException:
if(programmer instanceof Chinese) {
Chinese ch1 = (Chinese) programmer;
ch1.playShadowBoxing();
}else {
English en = (English) programmer;
en.raceHorse();
}
System.out.println(programmer instanceof Chinese); // false
System.out.println(programmer instanceof Programmer); // true
System.out.println(programmer instanceof Object); // true
}
}
注意:多态和方法有关,与属性无关
public class TestPoly2 {
public static void main(String[] args) {
Chinese chinese = new Chinese();
System.out.println(chinese.name); // ChinaName
Programmer program = new Programmer();
System.out.println(program.name); // proName
Programmer program2 = new Chinese();
System.out.println(program2.name); // proName
program2.eat(); // chinese eat
}
}
8.4. 简单工厂模式-返回值是父类类型
不仅可以使用父类做方法的行参,还可以使用父类做方法的返回值类型,真实返回的对象可以是该类的任意一个子类对象
public class WybSchool {
public static Programmer getProgrammer(String type) {
Programmer pro = null;
if("ch".equals(type)) {
pro = new Chinese();
}else {
pro = new English();
}
return pro;
}
}
public class TestPoly3 {
public static void main(String[] args) {
Programmer pro = WybSchool.getProgrammer("ch");
pro.eat(); // chinese eat
}
}
工厂模式是解决大量对象创建问题的一个解决方案。将创建和使用分开,工厂负责创建,使用者直接调用即可,简单工厂模式的基本要求:
- 定义一个static方法,通过类名直接调用
- 返回值类型是父类类型,返回的可以是其任意子类类型
- 传入一个字符串类型的参数,工厂根据参数创建对应的子类
9. final和抽象类
9.1. final
- final关键字的作用:
- 修饰变量:被final修饰的变量不可改变。一旦赋初值,不可重新赋值。final int MAX_SPEED = 120;
- 修饰方法:该方法不可被子类重写,但是可以重载。final void study();
- 修饰类:修饰的类不能被继承。比如:Math、String、System;final class A();
public class Maths {
private Maths() {
}
public static final double PI = 3.14159;
public static final double pow(int x, int y) {
double result = 1;
for (int i = 0; i < y; i++) {
result *= x;
}
return result;
}
public static double abs(double num) {
if(num >= 0) {
return num;
}else {
return -num;
}
}
}
- 注意:
- final不能修饰构造方法
- final修饰基本数据类型,值只能赋值一次
- final修饰引用数据类型,final Dog dog = new Dog(“大黄”); 不能变化的引用变量的值,可以变化的是对象的属性
public class Dog {
String name;
public Dog(String name) {
this.name = name;
}
}
public class Test {
public static void main(String[] args) {
final int NUM;
NUM = 5;
// NUM = 6;
System.out.println(NUM); // 5
final Dog dog;
dog = new Dog("大黄");
dog.name = "旺财";
System.out.println(dog.name); // 旺财
}
}
9.2. 抽象类
- 抽象方法:使用abstract修饰的方法,没有方法体,只有声明。定义的是一种规范,就是告诉子类必须要给抽象方法提供具体的实现
- 抽象类:使用abstract修饰的类。要求子类必须定义具体实现,通过抽象类,就可以做到严格限制子类的设计,使子类更加通用。
public abstract class Animal {
private String color;
public Animal() {
}
public Animal(String color) {
this.color = color;
}
public abstract void eat();
public String toString() {
return "Animal{color=" + color + '\'' + "}";
}
}
public class Dog1 extends Animal{
private String nickName;
public Dog1() {}
public Dog1(String color, String nickName) {
super(color);
this.nickName = nickName;
}
public void eat() {}
public String toString() {
return "Dog1{nickName-" + nickName + '\'' +"}" + super.toString();
}
}
抽象类的使用要点:
- 有抽象方法的类只能定义成抽象类
- 抽象类不能实例化,不能用new来创建抽象类
- 抽象类必须有构造方法,创建子类对象的时候使用
- 一个抽象类至少有0个抽象方法,至多所有方法都是抽象方法
- 子类必须重写父类的抽象方法,不重写就会编译错误;或者子类也定义为抽象类
10. 接口
接口就是定义一组规则,是一种规范
10.1. 接口
- 声明格式:
[访问修饰符] interface 接口名 [extends 父接口1, 父接口2...] {
常量定义;
方法定义;
}
- 定义接口详细说明:
- 访问修饰符:只能是public或默认
- 接口名:和类名命名机制相同
- extends:接口可以多继承
- 常量:接口中的属性只能是常量,总是:public static final修饰,不写也是
- 方法:接口中的方法只能是:public abstract。省略也是
- 要点:
- 子类通过implements实现接口中的规范
- 接口不能创建实例,但是可用于声明引用变量类型
- 一个类实现了接口,必须实现接口中的所有方法,并且这些方法只能是public的
- jdk1.8之前,接口中只能包含静态常量和抽象方法,不能有普通属性、构造方法和普通方法
- jdk1.8后,接口中包含普通的静态方法和默认方法
public interface Flyable {
public abstract void fly();
}
public class Plane implements Flyable{
public void fly() {
System.out.println("plane fly");
}
}
public class Animal {
}
public class Bird extends Animal implements Flyable{
public void fly() {
System.out.println("bird fly");
}
}
public class Test {
public static void showFly(Flyable fly) {
fly.fly();
}
public static void main(String[] args) {
Flyable plane = new Plane();
showFly(plane); // plane fly
Bird bird = new Bird();
showFly(bird); // bird fly
}
}
- 接口的组成
- 接口和数组、类、抽象类都是同一个层次的概念
- 成员变量:接口中所有变量都使用public static final修饰,都是全局静态常量
- 成员方法:接口中所有方法都使用public abstract修饰,都是全局抽象方法
- 构造方法:接口不能new,也没有构造方法
- 接口做方法的行参,实参可以接口的所有实现类
- 接口的多继承
- c++多继承
- 好处:从多个父类继承更多的功能
- 缺点:不安全,如果有两个父类,都有相同的方法,子类如果没有重写,使用哪个方法呢?
- java单继承
- 好处:安全
- 缺点:功能受限
- 解决方案:采用接口,接口变相的使java实现了c++的多继承,又没有c++多继承的不安全性
- 必须先extends再implements
- c++多继承
10.2. 接口应用:内部比较器Comparable
定义一个比较接口Comparable,其中定义一个比较方法compareTo(Object obj),让各个类实现该接口
public interface Comparable {
/*
* 判断两个对象的大小
* @param obj 另外一个对象
* @return
* > 0 正数 大于
* = 0 等于
* < 0 负数 小于
* */
public int compareTo(Object obj);
}
public class Book implements Comparable{
private String bookName;
private String author;
private String publisher;
private double price;
public Book(String bookName, String author, String publisher, double price) {
this.bookName = bookName;
this.author = author;
this.publisher = publisher;
this.price = price;
}
@Override
public int compareTo(Object obj) {
Book other = (Book) obj;
if(this.price > other.price) {
return 1;
}else if(this.price < other.price) {
return -1;
}else {
return 0;
}
}
}
public class Person implements Comparable{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Object obj) {
Person other = (Person) obj;
return this.age - other.age;
}
}
public class Test1 {
public static void main(String[] args) {
Book book1 = new Book("我是猫", "外国", "中信", 35);
Book book2 = new Book("我是猫", "外国", "中信", 30);
// boolean result = book1.equals(book2); // false
int result = book1.compareTo(book2); // 1
System.out.println(result);
Person person1 = new Person("wyb", 27);
Person person2 = new Person("xz", 32);
result = person1.compareTo(person2); // -5
System.out.println(result);
}
}
10.3. JDK1.8的接口新特性
- JDK7及其之前
- 接口的变量都是public final static全局静态常量,无变化
- 接口中都是抽象abstract方法,不能有static方法(因为abstract和static、final、private不能共存)
- JDK1.8及其之后
- 接口中可以添加非抽象方法static,实现类不能重写,只能通过接口名调用
- 如果子类中定义了相同名字的静态方法,那就直接从属于子类,可以通过子类名直接调用
- 接口中可以添加非抽象方法default,实现类可以重写,只能通过对象名调用
- 实现类可以直接使用default方法,也可以重写default方法,但是必须去掉default
- 上级接口中default方法的调用:MyInterface.super.method()
- 提供非抽象方法的目的
- 为了解决实现该接口的子类代码重复的问题
- 为了java类库的类增加新功能,且不必对这些类重新进行设计。
public interface MyInterface {
public static final double PI = 3.14;
public abstract void method1();
public static void method2(){
System.out.println("jdk1.8中,非抽象方法static");
}
public default void method3() {
System.out.println("jdk1.8中,非抽象方法default");
}
public static void main(String[] args) {
MyInterface.method2();
}
}
public class MyClass implements MyInterface{
@Override
public void method1() {
System.out.println("接口中的抽象方法,子类必须实现");
}
@Override
public void method3() {
MyInterface.method2(); // jdk1.8中,非抽象方法static
MyInterface.super.method3(); // jdk1.8中,非抽象方法default
System.out.println("重写接口的default方法,须将default去掉"); // 重写接口的default方法,须将default去掉
}
public static void main(String[] args) {
MyInterface mi = new MyClass();
mi.method1(); // 接口中的抽象方法,子类必须实现
MyInterface.method2(); // jdk1.8中,非抽象方法static
mi.method3();
}
}
10.4. 面向接口编程
面向接口编程是面向对象编程的一部分
接口就是规范,就是项目中最稳定的核心。面向接口编程可以把握住真正核心的东西,使实现复杂多变的需求成为可能。
通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,提高整个系统的可扩展性和可维护性
11. 内部类
内部类是一类特殊的类,指的是定义在一个类的内部的类,为了方便的使用外部类的相关属性和方法,这时候通常会定义一个内部类。
内部类主要分为:非静态成员内部类、静态成员内部类、局部内部类、匿名内部类
11.1. 非静态成员内部类
作为类的成员存在,和成员变量、成员方法、构造方法、代码块并列。因为是类的成员,所以非静态成员内部类可以使用public、protected、默认、private修饰,而外部类只能使用public、默认修饰
public class OuterClass {
private String name;
private int num = 10;
public OuterClass() {}
public OuterClass(String name, int num) {
this.name = name;
this.num = num;
}
public void methodOut() {
System.out.println("method");
}
public void methodOut1() {
// 外部类不可以直接访问内部的成员变量和成员方法
// System.out.println(type);
// methodInner();
InnerClass ic = new InnerClass();
System.out.println(ic.num); // 20
ic.methodInner(); // null 30 20 10 method
}
// 内部类
class InnerClass {
private String type;
private int num = 20;
public InnerClass() {}
public InnerClass(String type, int num) {
this.type = type;
this.num = num;
}
public void methodInner() {
// 内部类可以访问外部类的成员变量
System.out.println(name); // null
int num = 30;
System.out.println(num); // 30
System.out.println(this.num); // 20
// 内部类访问外部类的同名成员变量
System.out.println(OuterClass.this.num); // 10
methodOut();
}
}
}
public class Test {
public static void main(String[] args) {
OuterClass oc = new OuterClass();
oc.methodOut();
oc.methodOut1();
// 要创建非静态成员内部类对象,必须先创建外部类的对象
// OuterClass.InnerClass ic = new OuterClass().new InnerClass();
OuterClass oc1 = new OuterClass();
OuterClass.InnerClass ic = oc1.new InnerClass();
}
}
- 基本特征:
- 内部类可以直接访问外部类的成员
- 外部类不能直接访问内部类的成员,需要先创建对象再通过对象名访问
- 内部类访问外部类的同名成员变量:OuterClass.this.num
- 必须先创建外部类的对象,才能创建内部类的对象。非静态成员内部类是属于某个外部类对象的
- 更多特征
- 非静态内部类不能有静态方法、静态属性和静态初始化块
- 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例
注意:只要编译成功,内部类和外部类就会成为两个完全不同的类。Outer外部类和Inner内部类编译完成后是Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的存在,其成员变量/方法名可以和外部类相同
11.2. 静态成员内部类
public class OuterClass1 {
private static String name;
private static int num = 0;
public OuterClass1() {}
public OuterClass1(String name, int num) {
this.name = name;
this.num = num;
}
public static void methodOut() {
System.out.println("method out");
InnerClass1 ic = new InnerClass1();
InnerClass1.methodInner1();
}
public void methodOut2() {
// 外部类不可以直接访问内部的成员变量和成员方法
InnerClass1 ic = new InnerClass1();
System.out.println(ic.num); // 20
ic.methodInner();
InnerClass1.methodInner1();
}
static class InnerClass1 {
private String type;
private int num = 20;
public InnerClass1() {
}
public InnerClass1(String type, int num) {
this.type = type;
this.num = num;
}
public void methodInner() {
// 静态内部类只能访问外部类的静态成员
System.out.println(name); // null
int num = 30;
System.out.println(num); // 30
System.out.println(this.num); // 20
// 静态内部类访问外部类的同名的成员变量
System.out.println(OuterClass1.num); // 0
methodOut();
}
public static void methodInner1() {
}
}
}
import com.wyb.neibulei.OuterClass1.InnerClass1;
public class Test1 {
public static void main(String[] args) {
// 要创建静态成员内部类对象,不需要先创建外部类的对象
OuterClass1.InnerClass1 ic = new OuterClass1.InnerClass1();
ic.methodInner();
InnerClass1 ic1 = new InnerClass1();
ic1.methodInner();
}
}
- 总结
- 静态内部类只能够访问外部类的静态成员
- 静态内部类访问外部类的同名成员变量:OuterClass.num
- 静态内部类属于整个外部类。创建静态内部类对象,不需要先创建外部类的对象
- 外部类可以通过类名直接访问内部类的静态成员,访问非静态成员依旧需要先创建内部类对象
11.3. 局部内部类
定义在方法内部,作用域只限于本方法,称为局部内部类
局部内部类用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,又不希望这个类是公用的,就需要使用局部内部类。局部内部类和成员内部类一样被编译,只是他的作用域发生了改变,只能在该方法中被使用。
局部内部类在实际开发中应用很少
public class OuterClass2 {
int num = 10;
private void method() {
int num1 = 20;
class InnerClass {
public void method1() {
num = 100;
System.out.println(num1);
// num1 = 200;
}
}
InnerClass ic = new InnerClass();
ic.method1();
}
}
注意:局部内部类访问所在方法的局部变量,要求局部变量必须使用final修饰。JSK1.8中final可以省略,编译后会加final
11.4. 匿名内部类
匿名内部类是一种特殊的局部内部类
前提:存在一个类或接口,这里的类可以是具体类也可以是抽象类
本质是一个继承了该类或实现了该接口的子类匿名对象
适合只需要创建一次对象的类
- 语法:
new 父类构造器(实参列表) 实现接口() {
// 匿名内部类类体
}
public interface Comparator {
public int compare(Object obj1, Object obj2);
}
public class BookNameComparator implements Comparator {
@Override
public int compare(Object obj1, Object obj2) {
Book book1 = (Book)obj1;
Book book2 = (Book)obj2;
return book1.getBookName().compareTo(book2.getBookName());
}
}
public class BookPriceNameComparator implements Comparator {
@Override
public int compare(Object obj1, Object obj2) {
Book book1 = (Book)obj1;
Book book2 = (Book)obj2;
if(book1.getPrice() > book2.getPrice()){
return -1;
}else if(book1.getPrice()< book2.getPrice()){
return 1;
}else{
return book1.getBookName().compareTo(book2.getBookName());
}
}
}
如果某个外部比较器只使用一次或者很少的次数,就可以不提供专门的类,而是使用匿名内部类。
public class Test2 {
public static void main(String[] args) {
Comparable comp;
Comparator comp2;
Book book1
= new Book("倚天屠龙记1","金庸1","清华大学出版社",35);
Book book2
= new Book("倚天屠龙记5","金庸5","清华大学出版社",35);
int result = book1.compareTo(book2);
System.out.println(result);
Comparator cmp1 = new BookNameComparator();
result = cmp1.compare(book1,book2);
System.out.println(result);
Comparator cmp2 = new BookPriceNameComparator();
result = cmp2.compare(book1,book2);
System.out.println(result);
Comparator cmp3 = new Comparator() {
//代码块,每次创建对象的时候执行,并且早于构造方法执行
{
System.out.println("--匿名内部类通过代码块完成初始化操作---");
}
@Override
public int compare(Object obj1, Object obj2) {
Book book1 = (Book)obj1;
Book book2 = (Book)obj2;
return book1.getAuthor().compareTo(book2.getAuthor());
}
};
result = cmp3.compare(book1,book2);
System.out.println(result);
}
}
- 结论
- 匿名内部类可以实现一个接口,也可以继承一个类(可以是抽象类)
- 匿名内部类只能实现一个接口,而不是多个
- 必须实现所有的方法,匿名内部类不能是抽象类
- 匿名内部类不可能有构造方法,因为类是匿名的
- 匿名内部类没有访问修饰符
- 如果想实现构造方法的一些初始化功能,通过代码块实现
- 如果要访问所在方法的局部变量,该变量需要使用final修饰(JDK1.8可省略final)
- 内部类的作用
- 内部类提供了更小的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问
- 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。但外部类不能访问内部类的内部属性
- 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整
- 用匿名内部类实现回调功能。
- 内部类的使用场合
- 在只为外部类提供服务的情况下,优先考虑使用内部类
- 使用内部类间接实现多继承:每个内部类独立的继承一个类或实现某些接口,无论外部类是否已经继承了某个类或实现了某些接口,对于内部类没有影响