特点一,“万物”皆是类和封装
1,包装文件
先从你最熟悉的 C 语言开始
学 C 的时候,可能会写:
#include <stdio.h> int main() { printf("Hello"); return 0; }这里的思维比较“直接”:
程序
↓
函数
↓
执行代码
例如:
int add(int a, int b) { return a + b; }你可以直接定义一个函数。
Java 不喜欢你把一个普通函数直接扔在文件里。
比如你不能这样:
int add(int a, int b) {
return a + b;
}
Java 会报错。
Java 更倾向于:
把代码组织到类里面。
所以你要写:
class Calculator { int add(int a, int b) { return a + b; } }然后:
Calculator calculator = new Calculator(); int result = calculator.add(10, 20);你可以把它理解成:
C:
函数
↓
直接调用
Java:
类
↓
对象
↓
对象的方法
↓
调用
2,构造方法
构造方法最常见的用途:创建对象的时候直接传数据
比如我们不想每次写
Student student = new Student(); student.name = "张三"; student.age = 18;而是希望:
Student student = new Student("张三", 18);
那么可以写
class Student { String name; int age; Student(String name, int age) { this.name = name; this.age = age; } }然后:
Student student = new Student("张三", 18);
这就是构造方法非常重要的用途。
这里需要理解this
class Student { String name; int age; Student(String name, int age) { this.name = name; this.age = age; } }这里有两个name:
this.name
当前对象的成员变量。
name
构造方法的参数。
所以:
this.name = name;当前对象的name ← 外部传进来的name
3,封装
封装的核心:把数据藏起来
我们可以这样写:
class Student { private String name; private int age; }private,就是 Java 封装最核心的东西之一。
这个成员只能在当前类内部访问。
所以外部:
Student s = new Student(); s.age = -100;会直接报错。
因为:
Student类外部
↓
private age
↓
不允许直接访问
那外部怎么修改 age?
这时候就需要给外部提供一个“合法入口”。
class Student { private String name; private int age; public void setAge(int age) { if (age >= 0) { this.age = age; } } public int getAge() { return age; } }外部:
Student s = new Student(); s.setAge(20); System.out.println(s.getAge());这样就变成:
外部代码
↓
setAge(20)
↓
Student内部检查
↓
age = 20
所以封装不是简单的“private + getter/setter”
这是初学 Java 时非常容易产生的误解。
属性 private,然后提供 getter/setter,这就是封装。
这其实只是最常见的实现方式。
真正的思想是:
隐藏对象内部的实现细节,只向外部暴露必要的操作接口。
比如:
class BankAccount { private double money; public void withdraw(double money) { if (money <= 0) { return; } if (money > this.money) { return; } this.money -= money; } }特点二,变量方法修饰权限
python中只有简单的口头约定和名义上的私有(其实还是可以通过_object_访问)
而java对不同内容进行了不同的规定其中·包含了public,protect,空着,private
访问权限依次提升
一张图总结,先来说一下什么是一个包,什么不是一个包,就是是否在同一个包文件夹下,也可以直接看代码的初始行会带一个package表示目录
特点三,继承多态接口
1,继承
比如我们有:
动物
├── 狗
├── 猫
└── 鸟
动物都有,名字年龄,吃东西,睡觉
但是狗还有:
叫
猫还有:
喵
如果我们每个类都重新写:全部重复了。
所以我们可以把公共部分提取出来:
class Animal { String name; int age; void eat() { System.out.println("吃东西"); }然后:
class Dog extends Animal { void bark() { System.out.println("汪汪"); } }Dog 继承 Animal
所以继承就是对复用代码的继承和对新内容的扩增
2,对父类方法重写 @Override
例如:
class Dog extends Animal { @Override void makeSound() { System.out.println("汪汪"); } }这里 Dog 把父类的:
makeSound()
重新实现了一遍。
这叫:
方法重写(Override)
十一、@Override是什么?
你经常会看到:
@Override public void xxx() { }它是一个注解。
意思是告诉 Java:
“我这个方法是准备重写父类的方法。”
3,重载和重写千万别搞混
这个是 Java 初学者的高频坑。
Overload:重载
同一个类:
void test(int a) {}
void test(String a) {}
方法名一样,参数不同。
叫:
方法重载
继承还有一个非常重要的东西:super
super
它可以理解为:
代表父类部分。
例如:你想用父类的name内容,就可以直接super.name
class Animal { String name = "动物"; } class Dog extends Animal { String name = "狗"; void test() { System.out.println(name); System.out.println(super.name); } } super.namesuper():调用父类构造方法
这个非常重要。
例如:
class Animal { String name = "动物"; } class Dog extends Animal { String name = "狗"; void test() { System.out.println(name); System.out.println(super.name); } } super.name执行:Dog dog = new Dog();两个都会输出
创建子类对象的时候,要先完成父类部分的初始化。
可以理解成:
new Dog()
↓
先初始化 Animal
↓
再初始化 Dog
而且:
子类构造方法第一行默认会调用
super()。
Java 和 Python 的继承有什么区别?
Python 你可能见过:
class Dog(Animal): pass Java: class Dog extends Animal { }思想基本一样。但是 Java 有一个很重要的限制:
Java 的类只能直接继承一个父类。
比如:
class Dog extends Animal {
}
可以。
但是:
class Dog extends Animal, Pet {
}
不允许。
也就是说:
Java,一个类
↓
只能有一个直接父类
这叫:
单继承
那 Java 如果想实现多个能力怎么办?
Java 使用:
接口 Interface
实现接口
接口和继承结合起来,就是 Java 面向对象很重要的一部分。接口内容后面会细致的讲解
继承和封装到底是什么关系?
现在把你刚学的两个知识放在一起:
封装
private
↓
隐藏内部数据
↓
通过方法控制访问
继承
extends
↓
复用父类代码
↓
扩展父类功能
4,多态
先看一个让人疑惑的代码
我们刚才有:
class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃狗粮");
}
public void bark() {
System.out.println("汪汪");
}
}
现在:
Animal a = new Dog();
你可能第一反应:
a明明是Animal类型,为什么后面new Dog()?
这正是多态的核心。
先把这一句话吃透
Animal a = new Dog();
实际上可以拆成两部分:
Animal a
↓
变量 a 的类型是 Animal
new Dog()
↓
真正创建出来的对象是 Dog
所以:
左边决定“你能看到什么”,右边决定“实际上是谁”。
这是理解 Java 多态最重要的一句话。
假设:
class Animal { public void makeSound() { System.out.println("动物叫"); } } class Dog extends Animal { @Override public void makeSound() { System.out.println("汪汪"); } } class Cat extends Animal { @Override public void makeSound() { System.out.println("喵喵"); } }现在:
Animal a1 = new Dog(); Animal a2 = new Cat(); a1.makeSound(); a2.makeSound();结果:
汪汪
喵喵
那怎么调用 Dog 特有的方法?
可以进行:
向下转型
但是这里有一个问题,类型转变前提是我得是父子类关系,所以需要判断,不允许任意一个类随便转变
if (animal instanceof Dog) { Dog dog = (Dog) animal; dog.bark(); }5,多态最大的实际价值:解耦
这个词你以后学 Java Web 会经常遇到:
解耦
比如你写:
public void feedDog(Dog dog) { dog.eat(); }这个方法只能接收:
Dog
以后如果想支持 Cat:
public void feedCat(Cat cat)
又要写一个。
但是如果:
public void feedAnimal(Animal animal) { animal.eat(); }那么:
feedAnimal(new Dog()); feedAnimal(new Cat()); feedAnimal(new Bird());全部可以。
因为:
Dog ──┐
Cat ──┼──> Animal
Bird ─┘
所以调用者不需要知道具体是哪一种动物。
你现在应该建立的完整思维
把三个概念串起来:
面向对象
├── 封装
│ ↓
│ 保护数据和内部实现
├── 继承
│ ↓
│ 复用和扩展已有类
└── 多态
不同子类产生不同表现,实际上它们是连起来的。
6,接口
先说接口到底是什么
你可以先把接口理解成:
接口是一份“规范”或者“合同”,规定一个类必须具备哪些能力。
例如我们规定:
所有会飞的东西,都必须具备
fly()能力。
那么可以定义:
interface Flyable void fly(); }注意这里:
void fly();
没有方法体。它表达的是:(其实可以理解为这里其实是 public abstract void fly())
abstract是一个修饰指的是这是一个抽象类不需要内部填写具体内容
你只要实现 Flyable,就必须提供 fly() 的具体实现。
然后:
class Bird implements Flyable { @Override public void fly() { System.out.println("鸟在飞"); } }这就是接口最核心的思想。他和继承最大区别就是
接口可以是多个
7,接口 + 多态实现分块
这才是接口真正强大的地方。例如:
interface Animal { void eat(); } class Dog implements Animal { @Override public void eat() { System.out.println("狗吃狗粮"); } } class Cat implements Animal { @Override public void eat() { System.out.println("猫吃鱼"); } }然后:
Animal a1 = new Dog(); Animal a2 = new Cat(); a1.eat(); a2.eat();输出:
狗吃狗粮
猫吃鱼
这和你刚才学的:
Animal a = new Dog();
本质上是一样的。区别只是,之前:Animal 是父类现在:Animal 是接口
所以你可以把它理解成:
接口是实现多态的重要工具。
拿这个javaweb举例,Java Web 特别喜欢接口
假设你正在写一个用户系统。
你定义:
interface UserService { void addUser(); void deleteUser(); void updateUser(); }然后:
class UserServiceImpl implements UserService { @Override public void addUser() { // 具体实现 } @Override public void deleteUser() { // 具体实现 } @Override public void updateUser() { // 具体实现 } }然后其他代码:
UserService service = new UserServiceImpl();
调用者只需要知道:
UserService
不需要关心:
UserServiceImpl
具体是怎么实现的。
8,接口的default
default的作用
主要是为了解决:
接口升级时,不影响原来的实现类。
例如原来接口:
interface Animal {
void eat();
}
后来接口增加:
default void sleep() {
System.out.println("睡觉");
}
以前实现Animal的类不需要修改代码,也可以正常使用。
所以:
default方法
↓
接口可以增加新方法
↓
原来的实现类不用强制修改
↓
解决接口升级问题
3. 默认方法和抽象方法的区别
// 抽象方法
void show();
// 默认方法
default void show() {
System.out.println("show");
}
区别:
抽象方法:没有方法体,实现类必须重写。
默认方法:有方法体,实现类可以不重写。
注意,如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写
4. 如果实现类要重写默认方法
去掉default:
9,匿名内部类
有些类只需要一次匿名写入,比如这里重写一个接口的代码,不需要重新建立一个类inplements接口,再重写再创建再重写