今天算是早上晚上加班加点学爽了,希望能赶紧速通了吧,找个实习比啥都强。
继承、方法重写
extends表示继承,Java的类只能直接继承一个父类。
super(...)调用父类构造方法。this(...)调用本类另一个构造方法。
子类可以重写父类的方法,@override是给编译器看的标记,注意与重载的区别。
重写是子类重新实现父类中同签名的实例方法,重载是方法名相同但参数列表不同。
实例
java
public class Main {
public static void main(String[] args) {
Student s=new Student("Alice",90);
System.out.println(s.getName());
s.introduce();
}
}
class Person{
private String name;
Person(String name){
this.name=name;
}
public String getName(){
return name;
}
public void introduce(){
System.out.println("My name is "+name);
}
}
class Student extends Person{
private int score;
Student(String name,int score){
super(name);
this.score=score;
}
/* @Override
public void introduce(){
System.out.println("My name is "+getName()+" and my score is "+score);
}
@Override */
public void introduce(){
super.introduce();
System.out.println("My score is "+score);
}
}
多态
Person p = new Student("Alice",90);
p.introduce();
先看以上例子,这段代码的introduce执行父类还是子类?
答案是子类,因为要区分变量声明的类型和实际对象的类型。
Person p表示声明类型是Person,决定p能够调用哪些方法。new Student()表示实际对象是Student,决定被重写的实例方法执行哪个版本。
可以用Person类型的变量引用学生对象,这样不会把学生对象转换成一个普通人对象,也不会复制或者丢掉它的成绩。实际对象仍然是Student,只是你通过Person类的引用来引用它。
反过来则不能直接写:
Student s = new Person("Alice") //编译错误
同一个调用可以执行不同的实现,例如:
java
public class Main {
public static void main(String[] args) {
Person p1=new Student("小明");
Person p2=new Teacher("王老师");
p1.introduce();
p2.introduce();
}
}
class Person{
private String name;
Person(String name){
this.name=name;
}
public String getName(){
return name;
}
public void introduce(){
System.out.println("My name is "+name);
}
}
class Student extends Person{
Student(String name){
super(name);
}
public void introduce(){
System.out.println("我是学生:"+getName());
}
public void study(){
System.out.println("我要学习");
}
}
class Teacher extends Person{
Teacher(String name){
super(name);
}
@Override
public void introduce(){
System.out.println("我是老师:"+getName());
}
}
p1.study()不能成功调用。
因为编译器根据变量声明类型Person检查调用,Person声明了introduce,允许调用;没声明Study,不允许调用。因此要分两步看,编译时:根据声明类型检测这个方法能不能调用;运行时:对于重写的实例方法,根据实际对象选择执行哪个实现。
多态的用途
例如我们只写一个统一的介绍方法
static void showPerson(Person p){
p.introduce();
}
这一句话应该写在main()方法外面,和main()并列。
java
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Person> people = new ArrayList<>();
people.add(new Student("小明"));
people.add(new Teacher("老王"));
for(Person person:people){
person.introduce();
}
}
static void showPerson(Person p){
p.introduce();
}
}
class Person{
private String name;
Person(String name){
this.name=name;
}
public String getName(){
return name;
}
public void introduce(){
System.out.println("My name is "+name);
}
}
class Student extends Person{
Student(String name){
super(name);
}
public void introduce(){
System.out.println("我是学生:"+getName());
}
public void study(){
System.out.println("我要学习");
}
}
class Teacher extends Person{
Teacher(String name){
super(name);
}
@Override
public void introduce(){
System.out.println("我是老师:"+getName());
}
}
练习
Person p = new Student("小明");
p.introduce(); // ① 执行 Student 重写的 introduce()
p.study(); // ② 编译报错:Person 类型中没有 study()
Student s = new Student("小红");
s.study(); // ③ 正常执行 Student 的 study()
向下转型
从父类类型转成子类类型,叫向下转型。
Person p = new Student("小明");
p.study();//错误
Student s = (Student) p;
s.study();//正常执行
注意:没有创建新对象,也没有改变原对象。s和p仍然引用同一个学生对象,只是变量的声明类型不同。System.out.println(p==s)的输出是true。
不过转型必须符合对象的真实类型。
Person p = new Teacher("王老师");
Student s = (Student) p;
这段话能够通过编译但是运行时会抛出ClassCastException,因为类型转换不能把老师变成学生。
所以需要引入一个词instanceof(instance 名词 例子、实例)。
java
Person p = new Student("小明");
if(p instanceof Student){
Student s = (Student) p;
s.study();
}
可以直接写成
java
Person p = new Student("小明");
if(p instanceof Student s){
s.study();
}
这个叫做instanceof模式匹配,检查成功后,直接提供一个Student类型的变量s,省去手动转型。
所以分清来年规划总需求,调用父类已经声明、子类重写了的方法,如introduce(),直接调用,让多态选择实现即可;调用子类才有的方法,如study(),先检查类型,再向下转型。
接口
接口:规定对象能做什么,实现类提供具体做法。
class Student extends Person 表示学生是一种人,也就是人的扩展。
class Student implements Learnable 表示学生承诺提供Learnable接口要求的方法。
java
public class Main{
public static void main(String[] args){
Learnable a = new Student();
Learnable b = new Robot();
startLearning(a);
startLearning(b);
}
static void startLearning(Learnable obj){
obj.learn();
}
}
interface Learnable{
void learn();
}
class Student implements Learnable{
@Override
public void learn(){
System.out.println("学生通过看书学习");
}
}
class Robot implements Learnable{
@Override
public void learn(){
System.out.println("机器人通过数据训练学习");
}
}
接口里的void learn();是什么?
是实现这个接口的具体类,必须提供一个无参数、无返回值的learn()方法,这种接口方法默认就是public abstract,所以这里等价于public abstract void learn();实现时也必须写成public。而如果一个普通具体类写了class xxx implements Learnable却没有实现learn()就会编译报错。
implements就是明确声明实现关系,如果只写class Student漏了implements Learnable,那么在
Learnable obj = new Student();
会出现报错。因为Student没有声明实现Learnable。
最后可以对比一下Learnable obj=new Student();和Person p = new Student("小明"),它们两个高度相似,声明类型规定能调用的方法,实际对象决定执行哪个实现。
但是------接口本身不能直接创建对象,比如new Learnable();就是错误的,要创建具体实现类。
继承与实现也可以同时使用,学生既可以继承Person也可以实现Learnable。例子如下。
java
public class Main{
public static void main(String[] args){
Learnable a = new Student("孩子们");
Learnable b = new Robot();
startLearning(a);
startLearning(b);
}
static void startLearning(Learnable obj){
obj.learn();
}
}
interface Learnable{
void learn();
}
class Person{
private String name;
Person(String name){
this.name=name;
}
public String getName(){
return name;
}
}
class Student extends Person implements Learnable{
Student(String name){
super(name);
}
@Override
public void learn(){
System.out.println(getName()+"正在学习");
}
}
class Robot implements Learnable{
@Override
public void learn(){
System.out.println("机器人通过数据训练学习");
}
}
泛型
先看不使用泛型的类
java
class Box {
private String value;
public void set(String value) {
this.value = value;
}
public String get() {
return value;
}
}
它不能直接存学生对象或整数。如果每种类型都写一个盒子类,就会重复很多代码。可以把固定的String换成一个类型参数 T。
java
class Box<T> {
private T value;
public void set(T value){
this.value = value;
}
public T get(){
return value;
}
}
这里的T代表使用这个类时指定的类型。它不是字符串,也不是变量值,而是类型的占位符。
java
public class Main{
public static void main(String[] args){
Box<String> textBox = new Box<>();
textBox.set("hello");
String text = textBox.get();
Box<Integer> numberBox = new Box<>();
numberBox.set(123);
int number=numberBox.get();
System.out.println(text);
System.out.println(number);
}
}
class Box<T> {
private T value;
public void set(T value){
this.value = value;
}
public T get(){
return value;
}
}
不直接用Object作为父类存储所有东西的原因:取出时,返回类型只是Object,需要自己手动向下转型,而且容易出错。那么泛型就可以把这一类错误提前到编译阶段。
Box<String> box = new Box();
box.set(123); //直接编译报错
只要内部保持一致即可。常用命名习惯是:
| 名称 | 常见含义 |
|---|---|
T |
Type,某种类型 |
E |
Element,元素类型 |
K |
Key,键类型 |
V |
Value,值类型 |
用包装类的原因:Java泛型的类型参数必须是引用类型,所以使用时可以自动装箱拆箱。
练习
java
Box<String> a = new Box<>();
a.set("Java");
Box<String> b = a;
b.set("Python");
System.out.println(a.get());
逐步看:
| 执行语句 | 发生什么 |
|---|---|
new Box<>() |
创建一个盒子 |
a.set("Java") |
把盒子的 value 设为 "Java" |
b = a |
b 和 a 引用同一个盒子,没有创建新盒子 |
b.set("Python") |
把这个共同盒子的 value 改为 "Python" |
a.get() |
从同一个盒子取出 value,得到 "Python" |
这不违反字符串不可变的规则:我们没有把 "Java" 字符串对象改成 "Python",而是修改了盒子的 value 字段,让它引用另一个字符串。
现在学着越来越恶心我总结了一下我的问题:哦那我一直都理解错了,我一直纠结的他妈的这个叫包装类?
对,你一直想弄清楚的是 包装类------为什么有了 int,还要有 Integer。我之前把话题带到泛型类怎么定义上了,没对准你的疑问,抱歉。
这两个概念分开记:
- 包装类 :`Integer` 把整数值表示为对象,和基本类型
int对应。 - 泛型 :`ArrayList<T>` 中的类型参数机制,使用时可以指定
String、`Integer`、`Student` 等类型。
它们在这句里碰到一起:
ArrayList<Integer> nums = new ArrayList<>();
列表通过泛型指定元素类型;因为不能填基本类型 int,所以填包装类 Integer。
接下来应该把包装类、装箱和拆箱讲透,再继续其他内容。
包装类、装箱与拆箱
以下内容用int与Integer为例。
包装类:用对象和表示基本类型的值
int a=10;
Integer b = Integer.valueOf(10);
Integer.valueOf(10)是Integer类提供的静态方法,返回一个表示该数值的对象,可能复用已有对象。
装箱与拆箱:值和包装对象之间转换。
装箱是把int转成Integer,拆箱则是Integer取出int。
int a=10;
Integer b = Integer.valueOf(a);
Java支持自动装箱,所以可以简写Integer b=a;也可以自动拆箱int c=b。
Integer对象的数值不可修改,这点和String类似。
Integer用==比较容易出问题,因为Java缓存并复用一部分整数包装对象,不能据此认为这两个Integer的==比较的是数值,需要用a.equals(b)。
两个 Integer 之间的 == 比较对象身份;比较整数值,使用 equals(),或者明确拆箱后比较。
System.out.println(a.intValue() == b.intValue()); // true
如果其中一边本来就是 int,则会拆箱后比较:
Integer a = 1000;
int b = 1000;
System.out.println(a == b); // true:a 拆箱后比较数值
最后是null。
Integer a=null;
但这些操作需要拆箱,因此会抛出 NullPointerException:
int b = a;
a++;
System.out.println(a + 1);
例题
java
int x = 10;
Integer a = x;
Integer b = a;
x = 20;
b++;
System.out.println(x);
System.out.println(a);
System.out.println(b);
这次正确结果是 20、10、11。
int x = 10;
Integer a = x;
这里把 x 当时的值 10 装箱,让 a 引用表示 10 的对象。a 不会和变量 x 建立持续关联。
Integer b = a;
此时 a、`b` 引用同一个表示 10 的对象。
x = 20;
只修改变量 x,不会影响 a、`b`。
b++;
取出 b 的数值 10,加一,再让 b 引用表示 11 的对象。`a` 仍然表示 10。