IDE
集成开发环境(编写 + 编译 + 运行),常用:IDEA、Eclipse,下载链接如下:
下载 IntelliJ IDEA – 领先的 Java 和 Kotlin IDE (jetbrains.com)
IDEA 设置中文方式:
包
一种逻辑上的代码组织方式,作用如下:
- 逻辑分组:将相关类或接口组织在一起,便于查找和管理
- 命名空间:避免类名冲突,不同包下允许同名类
- 访问控制:比如default 访问修饰符表示只能在同包下访问
- 导入声明:要使用其他包中的类时,需要使用import 关键字进行导入
尽管包不是物理上的项目结构,但在实际的项目文件系统中,通常会根据包名来组织类文件(包名即文件名)
包的命名规则如下:
- 只能包含数字、字母下划线和小圆点
- 不能使用数字开头
- 不能使用关键字/保留字
- 一般为域名反写 + 项目名 + 模块名
Java 中常被使用的包如下:
- java.lang.*:基本包,默认引入
- java.util.*:工具包
- java.net.*:网络包
- java.awt.*:图形界面包
访问修饰符
Java 提供了四种访问修饰符来控制方法或属性的访问范围
- public:公开
- protected:同包和子类公开
- default:同包公开
- private:本类访问,不公开
修饰符用于修饰类、方法和属性,但类只能使用public 和default 修饰
封装 - private
封装是对 对象内部实现机制的隐藏,只暴露出对外访问的接口,用户只需与其进行互动即可完成需求。封装的优点如下:
- 隐藏数据:类的内部状态通常是隐藏的,只能通过类提供的方法才能进行访问。这有助于保护数据的完整性,避免外部代码直接修改对象的状态
- 提供接口:通过公共方法对外提供接口,隐藏方法内部的实现细节
- 访问控制:配合访问修饰符可以对类成员访问的限制
- 安全性:通过封装可以实现对访问数据的控制,更容易地实现数据验证和其他安全措施
- 模块化:有助于建立更模块化的代码,使得单个组件可以独立于其他部分进行开发和测试
public class Test {
// 私有化属性,避免外部代码直接访问
private int age;
private String name;
// 提供公开的方法共外界与属性进行交互
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
继承 - extends
在面向对象编程中,继承是一种让一个类(子类)具备另一个类(父类)属性和行为的机制。通过继承,可以实现父类代码的复用,并且使得代码更容易维护和扩展
子类虽然继承了父类的所有属性和行为,但是对于父类中私有的成员无法在子类中直接访问,需要通过父类提供的方法来完成访问
子类基于父类创建,因此在创建子类对象时,在子类构造器中会默认调用其父类的无参构造器。如果父类中没有无参构造器,则需要super 指定使用父类的有参构造器来完成对父类的初始化工作
如之前的this 指向当前对象,super 则是指向其父类对象。并且调用父类无参构造器的动作,即super() 会在构造器的第一行,而这和this 在构造器中的使用一致。正因如此,this 和super 在构造器中无法同时出现
使用super 的好处之一就是明确分工,子类和父类都通过各自的构造器来完成初始化;之二就是应对重名,当然在不重名的情况下,无论是使用super 还是this 都与直接访问类没有差别
Java 中所有的类都有一个共同的父类,即Object(祖宗类)。而一个类最多只能有一个父类(单继承),但是可以间接继承
需要注意,继承不能滥用,使用之前必须保证子类和父类之间是is - a 的关系,比如灰太狼是狼
创建子类对象流程的内存分析:
- 加载类信息:将当前类及其父类的类信息加载到方法区
- 分配内存:在堆中为对象分配内存,包括子类声明的实例变量和从父类中继承到的(除了私有成员变量)
- 初始化父类:子类构造器执行前,先调用父类构造器来初始化父类部分(隐式调用super() )
- 初始化子类:执行子类构造器,完成子类初始化
- 引用赋值:将对象实例的内存地址赋给引用变量
Override
即重写、也可以说是方法覆盖,也就是子类中存在除方法体外与父类完全相同的方法,就可以看作是子类重写了父类中的该方法
除了方法签名、返回值类型必须一致外,还需要保证子类重写方法的访问权限不能低于父类
与方法重载相同,重写也是对方法名的复用。它可以使子类以不同于父类的方式处理业务
属性不能被重写,如果在子类中定义了与父类相同的属性,则称其隐藏了父类中的对应属性
多态
多态是面向对象编程的一个核心概念,它允许同样的代码应用于不同类型的对象,以产生不同的行为。多态建立在封装和继承基础上(三大特征),本质上就是父类应用指向子类对象
多态可以分为以下两种:
- 编译时多态:通常通过方法重载(overloading)来实现,它允许在同一个类中定义多个同名方法,但要求这些方法具有不同的参数列表。选择哪个方法是在编译时决定的,基于调用时传递的参数类型和数量
- 运行时多态:最常见的一种多态形式,它主要通过继承和方法重写(覆盖)来实现。当子类继承自父类,并且重写了父类的方法时,就可以通过父类引用变量来调用子类的方法。具体调用哪个方法取决于运行时对象的实际类型,而不是引用变量的类型。这种特性使得程序可以根据实际对象的类型表现出不同的行为。
class Animal {
public void makeSound() {
System.out.println("Some generic sound");
}
}
class Dog extends Animal {
// 对Aninal 类方法的重写
public void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
// // 对Aninal 类方法的重写
public void makeSound() {
System.out.println("Meow!");
}
}
public class Main {
public static void main(String[] args) {
// Animal 是父类引用 -> 指向Dog 这个子类对象
// 实际运行时执行哪个方法取决于等号右边的对象类型,即运行时多态
Animal myAnimal = new Dog();
myAnimal.makeSound(); // 输出 "Woof!"
Animal myOtherAnimal = new Cat();
myOtherAnimal.makeSound(); // 输出 "Meow!"
}
}
上下转型
向上转型(子类用父类)需要遵循以下规则:
- 可以调用父类中除私有的成员
- 不能调用子类特有成员
- 具体可以调用的属性或行为由编译类型决定
class Animal {
private int age;
char sex;
public void makeSound() {
System.out.println("Some generic sound");
}
}
class Dog extends Animal {
private String name;
public void makeSound() {
System.out.println("Woof!");
}
}
public class Test{
public static void main(String[] args) {
// Dog 向上转型为Animal
Animal myAnimal = new Dog();
// 无法访问父类中私有的age,以及子类中特有的name
System.out.println("Dog sex is " + myAnimal.sex);
}
}
向下转型(父类使用子类)需要遵循以下规则:
- 父类引用必须指向当前类型的对象
- 只能转父类引用而不能转对象
- 可以调用子类的所有成员
class Animal {
private int age;
char sex;
public void makeSound() {
System.out.println("Some generic sound");
}
}
class Dog extends Animal {
private String name;
public void makeSound() {
System.out.println("Woof!");
}
}
public class Test {
public static void main(String[] args) {
// 注掉这一行就会报错,因为父类引用必须是当前类型对象,即Dog
Animal myAnimal = new Dog();
// Animal 向下转型为Dog
Dog dog = (Dog) myAnimal;
System.out.println(dog.sex);
}
}
区分向上或向下转型的方式很简单:在继承树中,父类在上、子类在下,父转子 -> 向下转型;子转父 -> 向上转型
之前提到属性不能被重写,具体的属性值取决于其编译类型
instanceof
比较操作符,用于判断左侧对象的类型是否为右侧类型或其子类,并且判断的是运行时类型
class Animal {
private int age;
char sex;
public void makeSound() {
System.out.println("Some generic sound");
}
}
class Dog extends Animal {
private String name;
public void makeSound() {
System.out.println("Woof!");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a instanceof Animal); // true
System.out.println(a instanceof Dog); // true
}
}
动态绑定
动态绑定也是面向对象编程中的一个关键概念。它是指在程序运行过程中决定调用哪个方法的过程,而不是在编译阶段就确定下来,这通常与继承和方法重写相关。具体规则如下:
- 当调用对象方法的时候,该方法回合运行类型对应对象的内存地址进行绑定
- 当调用对象属性的时候,没有动态绑定机制,遵循就近原则
class Animal {
public String name = "Generic Animal";
public void makeSound() {
System.out.println(name + ": Some generic sound");
eat();
}
public void eat() {
System.out.println("Eating ome generic food");
}
}
class Dog extends Animal {
public String name = "Dog";
public void eat() {
System.out.println(name + ": Eating dog food");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
a.makeSound(); // Generic Animal: Some generic sound
// Dog: Eating dog food
}
}
多态数组
即数组中元素为数组类型的子类,比如Animal[] arr = {cat, dog, bird...}
此时其编译类型为Animal,无法直接访问具体属性的属性或行为(Animal 有就访问Animal 的,没有就编译错误)。所以就需要向下转型,而为了保证类型转换正确,在转换之前需要使用instanceof 比较符进行类型判断
class Animal {
public String name = "Generic Animal";
public void makeSound() {
System.out.println(name + ": Some generic sound");
}
}
class Dog extends Animal {
public String name = "Dog";
public void eat() {
System.out.println(name + ": Eating dog food");
}
}
class Cat extends Animal {
public String name = "Cat";
public void eat() {
System.out.println(name + ": Eating cat food");
}
}
public class Test {
public static void main(String[] args) {
Animal[] animals = { new Dog(), new Cat()};
for (int i = 0; i < animals.length; i++) {
if (animals[i] instanceof Dog) {
((Dog) animals[i]).eat(); // Dog: Eating dog food
} else if (animals[i] instanceof Cat) {
((Cat) animals[i]).eat(); // Cat: Eating cat food
} else {
System.out.println("类型有误");
}
}
}
}
多态参数
与多态数组类似,实参类型允许为形参的子类型
class Animal {
public String name = "Generic Animal";
public String getName() {
return name;
}
}
class Cat extends Animal {
public String name = "Cat";
public String getName() {
return name;
}
}
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.walking(new Cat()); // Cat is walking
test.walking(new Animal()); // Generic Animal is walking
}
public void walking(Animal animal) {
System.out.println(animal.getName() + " is walking");
}
}