面向过程和面向对象
面向过程:将程序看作一系列步骤或方法的集合,程序通过依次执行这些步骤来完成任务。
- 以方法为中心,强调过程和逻辑。
- 数据与操作分离,数据通常作为参数传递给方法。
优点:
- 简单直观,符合人类解决问题的线性思维。
- 对于小型程序,开发速度快,代码量少。
缺点:
- 随着程序规模增大,代码的复杂性和维护成本急剧上升。
- 难以复用代码,修改功能时容易引入错误。
- 数据和操作分离,导致代码的耦合性较高。
面向对象:将程序看作一系列对象的集合,对象之间通过消息传递进行交互。每个对象都有自己的属性(数据)和行为(方法)。
- 以对象为中心,强调数据封装和模块化。
- 通过类和对象组织代码,支持继承、封装和多态。
优点:
- 代码结构清晰,易于维护和扩展。
- 支持代码复用(通过继承、组合等方式)。
- 数据与操作封装在一起,降低了耦合性。
缺点:
- 对于简单问题,可能会显得过于复杂。
以爬虫为例:
对于最简单的爬虫,爬取一个网页,只保存为图片,易于解析网页和解密。此时三个方法,分别是发起请求,解析网页,保存图片,足够完成整个程序的功能。此时面向过程最为合适,面向对象则过于复杂化:
def request(url):
# 发起请求
pass
def parse(html):
# 解析网页
pass
def save_image(data):
# 保存图片
pass
# 主程序
url = "http://example.com"
html = request(url)
data = parse(html)
save_image(data)
但是对于增加功能,如增加日志记录、异常处理、多线程、异步,以及多网页处理,多种格式保存,数据库交互,多种的解析方式和反反爬虫,不同的请求方式,对于面向过程而言过于复杂,在后期需要维护代码,以及复用相关代码都比较困难。而面向对象通过分为多种对象,分别维护不同的功能,维护性和复用性都很良好。
将不同功能模块化为对象:
- 请求模块:负责发起请求,处理请求参数、请求头等。
- 解析模块:负责解析网页内容,支持多种解析方式。
- 存储模块:负责保存数据,支持多种格式(图片、文本、数据库)。
- 日志模块:负责记录运行日志。
- 异常处理模块:负责捕获和处理异常。
- 调度模块:负责多线程、异步任务的调度。
上述的模块化也只是一个大概的抽象,实际上会更加复杂,比如python的爬虫框架Scrapy
。
def request(url):
# 发起请求
pass
def parse(html):
# 解析网页
pass
def save_image(data):
# 保存图片
pass
# 主程序
url = "http://example.com"
html = request(url)
data = parse(html)
save_image(data)
但是对于增加功能,如增加日志记录、异常处理、多线程、异步,以及多网页处理,多种格式保存,数据库交互,多种的解析方式和反反爬虫,不同的请求方式,对于面向过程而言过于复杂,在后期需要维护代码,以及复用相关代码都比较困难。而面向对象通过分为多种对象,分别维护不同的功能,维护性和复用性都很良好。
将不同功能模块化为对象:
- 请求模块:负责发起请求,处理请求参数、请求头等。
- 解析模块:负责解析网页内容,支持多种解析方式。
- 存储模块:负责保存数据,支持多种格式(图片、文本、数据库)。
- 日志模块:负责记录运行日志。
- 异常处理模块:负责捕获和处理异常。
- 调度模块:负责多线程、异步任务的调度。
上述的模块化也只是一个大概的抽象,实际上会更加复杂,比如python的爬虫框架Scrapy
。
类和对象
类
定义一个类的语法如下:
[访问修饰符] class 类名 {
// 成员变量
// 构造方法
// 成员方法
}
其中[xxx]
的内容是可选的。
示例:
// 新建一个Cat类
class Cat {
int age;
String name;
}
在一个Java文件中,只能有一个访问修饰符为public
的类。
类的使用
实例化一个类的语法如下:
类名 对象名 = new 类名(参数);
// 也可以:
类名 对象名;
对象名 = new 类名(参数);
示例:
Cat cat;
cat = new Cat();
Cat cat = new Cat;
成员变量
在类中创建一个成员变量的语法如下:
[访问修饰符][final] 数据类型 变量名 [= 初始值];
其中[xxx]
的内容是可选的。
示例:
class Cat {
int age = 2;
String name;
}
成员变量的使用
通过对象名.变量名
的方式访问成员变量,示例:
Cat cat = new Cat();
cat.name = "小艺";
System.out.println(cat.age); // 2
System.out.println(cat.name); // 小艺
成员变量即使没有初始值,也会有默认初始化的值,int
为 0,double
为 0.0,boolean
为 false,char
为\u0000,引用类型为null。
成员方法
在类中创建一个成员方法的语法如下:
[访问修饰符] [返回值类型] 方法名([参数列表]) {
// 方法体
[return 返回值;]
}
其中[xxx]
的内容是可选的。示例:
class Cat {
int age;
String name;
public void eat() {
System.out.println("eat");
}
}
public Main {
public static int add(int a, int b) {
return a+b;
}
}
相当于把传进来的变量赋值给参数,比如在上面的例子中,就是把传进来的两个数分别赋值给a和b。但如果传递的是引用数据类型,那么参数和被传递的变量指向同一处地址,此时参数修改对应地址处的数据会影响到被传递的变量。
当方法执行到第一个return
时,就会返回并结束方法的执行,即使后面可能还有return
。
成员方法的使用
通过对象名.方法名(参数)
的方式使用成员方法,示例:
Cat cat = new Cat();
cat.eat();
局部变量
局部变量是在 方法、构造方法或代码块 内部声明的变量,它的作用范围 仅限于定义它的代码块。和成员变量相比,定义的语法是数据类型 变量名 [= 初始值]
。局部变量在声明后必需初始化,不会被默认初始化。
成员方法内使用成员变量
在同一个类的成员方法内,成员变量可以直接使用,无需 this
关键字;如果成员变量与局部变量重名,则优先访问局部变量,若要访问成员变量,需要用 this.变量名
。
public class Example {
private int number = 10; // 成员变量
public void showNumber() {
System.out.println("成员变量 number = " + number); // 直接访问成员变量
}
public void setNumber(int number) { // 形参与成员变量同名
System.out.println("局部变量 number = " + number); // 访问的是局部变量
System.out.println("成员变量 number = " + this.number); // 访问成员变量
this.number = number; // 使用 this 赋值给成员变量
}
public static void main(String[] args) {
Example obj = new Example();
obj.showNumber();
obj.setNumber(20);
obj.showNumber();
}
}
成员方法调用成员方法
直接使用方法名调用,或者通过this.
方法名进行调用,两种方式效果相同。
public class Example {
private int number = 10;
public void showNumber() {
System.out.println("成员变量 number = " + number);
}
public void display() {
System.out.println("调用 showNumber 方法");
showNumber(); // 直接调用成员方法
this.showNumber(); // 使用 this 调用成员方法
}
public static void main(String[] args) {
Example obj = new Example();
obj.display();
}
}
方法重载
在同一个类中,可以定义多个同名方法,只要它们的参数列表不同。
- 参数类型不同:
public static void add(int a, int b) {
System.out.println(a + b);
}
public static void add(double a, double b) {
System.out.println(a + b);
}
- 参数数量不同:
public static void add(int a, int b) {
System.out.println(a + b);
}
public static void add(int a, int b, int c) {
System.out.println(a + b + c);
}
- 参数类型顺序不同,与变量名无关:
public static void add(int a, double b) {
System.out.println(a + b);
}
public static void add(double a, int b) {
System.out.println(a + b);
}
递归
**递归:**一个方法直接或间接调用自身的过程。
基本结构如下:
public void recursiveMethod(parameters) {
// 基准条件
if (baseCaseCondition) {
// 处理基准条件
return;
}
// 递归条件
recursiveMethod(modifiedParameters);
}
也可以将void
替换成返回值的数据类型,前提是递归方法必需在最后返回相同数据类型的返回值。
示例:
计算阶乘:
public int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * factorial(n - 1);
}
计算斐波那契数列:
public int fibonacci(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
注意事项:
必需注意递归的深度和条件,防止无限递归或者递归导致超过栈的大小。
可变参数
可以接收多个变量的参数,语法为:参数类型... 参数名
,其中的参数名
可以被当作数组使用,可变参数的实参可以为0个或任意多个。
示例:
public class Main{
public static void main(String[] args) {
int num = sum(1,2,3,4,5,6,7,8,9,10);
System.out.println(num); // 输出55
}
public static int sum(int... nums){
int temp = 0;
for(int i: nums) {
temp += i;
}
return temp;
}
}
可变参数的实参也是数组,在上面的例子中:
int[] numbers = {1,2,3,4,5,6,7,8,9,10};
int num = sum(numbers);
System.out.println(num); // 输出55
可变参数的本质就是数组。
注意事项:
一个方法中只能有一个可变参数,并且可变参数必需放到其它参数的最后。
作用域
一段程序代码中所用到的变量并不总是有效/可用的,而限定这个变量的可用性的代码范围就是这个变量的作用域。在Java中,作用域主要是通过{ }
来定义的,比如:
public class Main{
int a = 1;
public static void main(String[] args) {
{
static int b = 2;
System.out.println(a + b);
}
System.out.println(a);
}
}
在main
方法中的{}
内可以使用变量a,在{}
外无法使用变量b。
不同的作用域也关系着变量的声明周期,也就是变量从创建到被销毁的一段时间。在上述的例子中,变量a从程序开始执行到结束,变量b从进入{}
执行,到{}
中的代码执行完毕。
Java中的作用域主要分为以下几种:
- 类作用域:
- 适用范围:类级别的变量(成员变量)。
- 生命周期:随对象的创建而存在,随对象的销毁而消失。
- 方法作用域:
- 适用范围:方法内部定义的变量(局部变量)
- 生命周期:随方法调用而创建,随方法执行完毕而销毁。
- 块作用域:
- 适用范围:代码块
{}
内的变量。典型场景:if 语句、for/while 循环、try-catch 代码块。 - 生命周期:进入块时创建,退出块后销毁。
- 适用范围:代码块
- 静态作用域:
- 适用范围:
static
变量和方法,属于 类本身。 - 生命周期:程序启动时创建,程序结束时销毁。
- 适用范围:
构造方法
构造方法是类的一种特殊的成员方法,用于在创建对象时初始化对象,也就是在创建对象的时候会被执行且被最先执行。
基本语法:
[访问修饰符] 类名(参数) {
// 方法体
}
- 构造方法的名称必须与类名完全相同。
- 构造方法没有返回类型(连
void
也不能写)。
public class Animal {
String name;
public Animal(String name) {
this.name = name
}
}
如果类中没有显式定义构造方法,Java 会提供一个默认的无参构造方法。
构造方法的使用
在创建对象时,传递构造方法的参数。
示例:
public class Main{
public static void main(String[] args) {
Animal animal = new Animal("Dog");
System.out.println(animal.name); // 输出Dog
}
}
构造方法的重载
构造方法可以重载,在创建对象的时候传递不同的参数调用不同的构造器。
public class Main{
public static void main(String[] args) {
Solution s = new Solution(1); // 输出1
Solution s1 = new Solution(1.0); // 输出1.0
Solution s2 = new Solution("s1"); // 输出s1
}
}
class Solution {
public Solution(int a) {
System.out.println(a);
}
public Solution(double a) {
System.out.println(a);
}
public Solution(String a) {
System.out.println(a);
}
}
可以通过this(参数)
调用其它的构造方法,但必需写在第一行,并且只能出现一次调用其它的构造方法,只能在构造方法内部才能调用构造方法,比如:
class Solution {
public Solution() {
this(0, 0);
}
public Solution(int a, int b) {
System.out.println(a+b);
}
}
public class Main{
public static void main(String[] args) {
Solution s = new Solution(); // 输出0
Solution s1 = new Solution(1, 2); // 输出3
}
}
如果类中没有构造方法,Java会默认隐式的为类添加一个空的构造方法。
this关键字
this
用于引用当前对象的实例,访问当前对象的成员变量、成员方法、构造方法。
访问当前类的实例的成员变量和成员方法,直接通过this.
加上变量名和方法名进行调用。
public class Main{
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("Buddy");
System.out.println(dog.getName()); // Output: Buddy
}
}
class Dog {
private String name;
public void setName(String name) {
setName(this, name);
}
public String getName() {
return name;
}
private static void setName(Dog dog, String name) {
dog.name = name;
}
}
在上述的例子中,setName(Dog dog, String name)
方法接收一个Dog
对象进行命名,在调用时直接传入this
表示当前对象,改变当前对象name变量的值。
包
包的语法如下:
package 包名;
包实际上就是一个文件夹,导入不同文件夹下的类,可以避免类的重名问题,更好的组织代码结构。
示例:
package com.animal;
package com.pets;
如果在Animal下有一个Dog类,在Pets下也有一个Dog类,都导入就会发生冲突。
那么在定义的时候可以:
public class Main {
public static void main(String[] args) {
Pets.Dog dog = new Pets.Dog();
Animal.Dog dog2 = new Animal.Dog();
}
}
代码结构如下:
└───src
├───Animal
|───Pets
└───Main.java
访问修饰符
Java中有四种权限修饰符:private
、默认(不写)、protected
、public
同一个包下指的是同一个包下的.java
文件,如果包下面还有包,则属于不同的包,具体而言和package
后面定义的有关,例如package com.pets
和package com.pets.animals
,后者在前者的目录内,但是不一样,不是同一个包。
修饰类
只有**默认(不写)**和public
才可以修饰类,**默认(不写)**修饰的类只能在同一个包下被使用,在其它包下会找不到这个类,public
在任何地方都可以使用,包括:继承、实例化等。
修饰变量和方法
-
private
:修饰的变量及方法仅限在定义它们的类内部访问或调用,不能被子类或其他类访问。 -
默认(不写):修饰的变量及方法仅限在定义它们的类所在包下的类访问或调用。
-
protected
:修饰的变量及方法仅限在定义它们的类所在包下的类访问或调用,以及不同包下的子类可以访问或调用。 -
public
:修饰的变量及方法在任何地方都可以访问或调用。
封装
封装就是将 对象的状态(属性)和行为(方法) 绑定在一起,并通过 访问控制 限制外部对对象的直接访问。可以隐藏实现细节、添加校验逻辑,防止无效数据赋值。
步骤:
- 将属性进行私有化
- 提供一个公共(public)的set方法,用于对属性判断并赋值
- 提供一个公共(public)的get方法,用于获取属性的值
示例:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 1 && age <= 120) {
this.age = age;
} else {
System.out.println("Age must be between 1 and 120");
}
}
}
和构造器一起使用,使用setXXX方法设置成员变量的值:
public Person(String name, int age) {
setAge(age);
setName(name);
}
继承
继承是面向对象编程(OOP)的一种机制,允许一个类(子类 / 派生类)继承另一个类(父类 / 超类)的属性和方法,增强代码的复用性和可扩展性。
假设要实现一个企业不同员工的信息管理,不使用继承需要依次写董事长、管理员、一般员工等,这些类中代码的重复性会比较高,比如属性:姓名、ID、请假天数、基本工资等。可以实现一个staff
类,其它的类继承该类,对于类中不同的,可以在对应的类中在具体实现,提高了代码的维护性和复用性。
语法如下:
class Parent {
// 父类代码
}
class Child extends Parent {
// 子类代码
}
Java中所有类都是Object
的子类,即使不显示的继承,Java也会默认隐式地继承Object
类。
示例:
public class Main {
public static void main(String[] args) {
B b = new B();
b.methodB(); // 先输出B,在输出A
b.info(); // Tom
}
}
class A {
String name = "Tom";
public void methodA() {
System.out.println("A");
}
}
class B extends A {
public void methodB() {
System.out.println("B");
this.methodA();
}
public void info() {
System.out.println(this.name);
}
}
子类可以使用父类的属性和方法,像自己的属性和方法一样使用,但子类依然可以有自己的属性和方法,并且可以"替换"掉父类的属性和方法。即在使用属性和方法的时候,如果子类没有,就会去使用父类的,如果子类有,则优先使用子类的,这时可以使用super
指定访问父类的成员变量或调用成员方法。
// 父类
class Parent {
public String name = "Parent Name"; // 父类属性
public int age = 7;
public void show() { // 父类方法
System.out.println("Parent show()");
}
}
// 子类
class Child extends Parent {
public int age = 10; // 隐藏父类的age成员变量
@Override
public void show() { // 子类重写(替换)父类方法
System.out.println("Child show()");
}
}
public class Test {
public static void main(String[] args) {
Child child = new Child();
// 直接使用父类的属性
System.out.println(child.name); // 输出: Parent Name
// 直接使用父类的方法(如果未重写的话)
child.show(); // 输出: Child show() (子类替换了父类的方法)
// 子类自己的属性
System.out.println(child.age); // 输出: 10
}
}
子类继承了父类的所有成员变量和方法(构造方法除外),但是会受到访问修饰符的限制,即使 private
修饰的变量和方法不能直接访问,但仍然存在于子类对象的内存中,可以通过反射间接获取。
继承后的 查找顺序:子类 -> 父类 -> 祖父类 -> ... -> Object。
class GrandParent {
public void show() { System.out.println("GrandParent show"); }
}
class Parent extends GrandParent {
public void show() { System.out.println("Parent show"); }
}
class Child extends Parent {
public void sayHello() { System.out.println("Child sayHello"); }
}
public class Test {
public static void main(String[] args) {
Child child = new Child();
child.show(); // 输出: "Parent show"
}
}
Java中的继承是单继承机制,即一个类只能继承一个父类;不能滥用继承,必需满足is-a的条件,例如:Cat extends Animal
可以,但是Music extends Animal
并不合适。
子类可以通过super
调用父类的构造方法,子类不会继承父类的构造方法,但子类的构造方法必须调用父类的构造方法,以确保父类的部分被正确初始化;如果子类没有显式的调用父类的构造方法,子类默认会隐式地调用父类的无参构造方法;如果父类没有无参构造方法,子类必须显式调用带参构造方法,否则编译报错。构造器的执行顺序:先执行父类构造器 → 再执行子类构造器。
super关键字
super
用于引用当前对象父类的实例,访问父类的成员变量、成员方法、构造方法。
调用成员变量和成员方法,通过super.
加上变量名或方法名。
调用父类的构造器,无参构造器super()
,有参构造器super(参数列表)
。
注意事项:
-
子类构造方法默认会隐式地调用父类的无参构造方法
super();
(即使代码中没有写)。 -
如果父类没有无参构造方法,子类必须手动调用带参数的
super(参数...)
,否则编译错误。 -
super(...)
只能在构造方法的第一行,否则编译错误。 -
this(...)
也只能在构造方法的第一行,所以子类的一个构造方法中,super(...)
和this(...)
只能用一个。
变量的隐藏
子类可以定义和父类同名的变量,隐藏父类的成员变量或类,在不使用super
或通过父类访问类变量的情况下,默认访问的将会是子类的成员变量,对访问修饰符无要求,对是否是成员变量和类变量无要求。只要两个变量同名即可,成员变量可以隐藏类变量,反之也可以,成员变量之间和类变量之间也可以。
示例:
import java.io.IOException;
public class Main {
public static void main(String[] args) {
new Child().show();
}
}
class Parent {
int value = 10;
static int num = 100;
static String name = "父类的静态变量";
int id = 10;
}
class Child extends Parent {
int value = 20;
static int num = 200;
String name = "子类的成员变量";
static int id = 20;
void show() {
System.out.println("子类的 value:" + value);
System.out.println("父类的 value:" + super.value);
System.out.println("子类的 num:" + num);
System.out.println("父类的 num:" + Parent.num);
System.out.println("子类的 name:" + name);
System.out.println("父类的 name:" + Parent.name);
System.out.println("子类的 id:" + id);
System.out.println("父类的 id:" + super.id);
}
}
上述代码的执行结果为:
子类的 value:20
父类的 value:10
子类的 num:200
父类的 num:100
子类的 name:子类的成员变量
父类的 name:父类的静态变量
子类的 id:20
父类的 id:10
成员方法重写
方法重写是子类对父类中非 private
修饰的成员方法进行重新定义,使其行为符合子类的需求。
要求:
- 方法名相同,参数列表(方法签名)相同。
- 返回值类型相同或为父类方法返回值的子类
- 访问权限不能比父类更严格
- 子类方法声明的异常不能比父类方法声明的更广泛,父类不声明异常则子类不能声明异常
- 都必须是成员方法
示例:
class Parent {
void show() {
System.out.println("父类的方法");
}
}
class Child extends Parent {
@Override
void show() { // 方法重写
System.out.println("子类的方法");
}
}
public class Test {
public static void main(String[] args) {
Parent p = new Parent();
p.show(); // 输出:父类的方法
Child c = new Child();
c.show(); // 输出:子类的方法
Parent pc = new Child(); // 父类引用指向子类对象(多态)
pc.show(); // 输出:子类的方法(调用的是子类的重写方法)
}
}
在子类重写的方法上方加上@Override
,可以让编译器检查确保子类正确重写了父类方法。
类方法的隐藏
要求:
- 方法名相同,参数列表(方法签名)相同。
- 都必须是类方法,类方法和成员方法之间会报错
- 返回值类型相同或为父类方法返回值的子类
- 访问权限不能比父类更严格
- 子类方法声明的异常不能比父类方法声明的更广泛,父类不声明异常则子类不能声明异常
示例:
public class Main {
public static void main(String[] args) {
Parent.show(); // 调用父类的静态方法
Child.show(); // 调用子类的静态方法
System.out.println(Parent.getMessage()); // 访问父类的静态方法
System.out.println(Child.getMessage()); // 访问子类的静态方法
}
}
class Parent {
static void show() {
System.out.println("父类的静态方法 show()");
}
static String getMessage() {
return "父类的 getMessage()";
}
}
class Child extends Parent {
static void show() { // 隐藏父类的静态方法
System.out.println("子类的静态方法 show()");
}
static String getMessage() { // 隐藏父类的静态方法
return "子类的 getMessage()";
}
}
上述代码的执行结果为:
父类的静态方法 show()
子类的静态方法 show()
父类的 getMessage()
子类的 getMessage()
多态
多态是同一个行为具有多个不同表现形式或形态的能力。在Java中,就是一个引用类型的变量,所指向的具体的类型和通过该变量调用的方法,在编译时不确定,在运行期间才确定。在效果上,就是一个父类的实例,可以指向该父类不同的子类,从而在调用父类的方法时,表现出不同的效果。
Java允许声明一个父类的变量,但实际在内存中指向的,即new
后面跟的是子类,这种方式称为向上转型。其中=
前面的是编译类型,在编译期间被作为该类型;后面的是运行类型,即实际的类型。
class Parent {
public void say() {
System.out.println("Parent say");
}
}
class Child extends Parent {
@Override
public void say() {
System.out.println("Child say");
}
public void looK() {
System.out.println("Child look");
}
}
public class Main {
public static void main(String[] args) {
Parent p = new Child();
p.say(); // Child say
p.look; // 报错
}
}
此时变量p
可以调用的属性和方法都是父类的,否则在编译时会无法通过,因为它的编译类型是Person
,虽然内存中实际是Child
。
但是如果调用被子类重写的say
方法,最终输出的是Child say
。即如果以这种方式创建的变量,调用被子类重写的方法,实际调用的是子类的方法,在效果上等同于子类的对象去调用该方法。
因为p实际上在内存中指向的是Child
类型的,所以可以在转换成Child
类型,称为向下转型,必需现有向上转型,才可以向下转型,示例如下:
public class Main {
public static void main(String[] args) {
Parent p = new Child();
Child c = (Child) p;
}
}
但如果不是内存中指向的不是Child
类型,就会报错,在进行向下转型前可以先用instanceof
进行判断:
public class Main {
public static void main(String[] args) {
Parent p = new Child();
if (p instanceof Child) {
Child c = (Child) p;
}
}
}
当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)绑定,称为动态类型绑定。在效果上就是,如果调用没有被子类重写的方法,相当于一个父类的对象调用该方法,所使用的成员变量是父类的,方法内部调用的方法,即使被子类重写了也是父类的;如果调用子类的方法,相当于一个子类的对象调用该方法,所使用的成员变量是子类的,方法内部调用的方法也是子类的。示例如下:
class Parent {
int i = 10;
public int getI() {
return i;
}
public int sum() {
return 20 + getI();
}
}
class Child extends Parent {
int i = 20;
@Override
public int getI() {
return i;
}
}
public class Main {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.sum()); // 30
System.out.println(p.getI()); // 20
}
}
对于成员变量,子类不会影响到父类,根据声明的编译类型进行访问,即使是子类中存在同名的成员变量。
class Parent {
String name = "父类的name";
}
class Child extends Parent {
String name = "子类的name"; // 隐藏父类的 name 变量
}
public class Test {
public static void main(String[] args) {
Parent p = new Parent();
System.out.println(p.name); // 输出:父类的name
Child c = new Child();
System.out.println(c.name); // 输出:子类的name
Parent p2 = new Child();
System.out.println(p2.name); // 输出:父类的name(变量隐藏,不受多态影响)
}
}
instanceof
运算符
instanceof
运算符用于判断变量的运行时类型是不是指定类的实例,以及指定类子类的实例,语法为:对象 instanceof 类名
。
class Parent {}
class Child extends Parent {}
public class Test {
public static void main(String[] args) {
Parent p = new Parent();
Child c = new Child();
Parent pc = new Child(); // 向上转型
System.out.println(p instanceof Parent); // true
System.out.println(c instanceof Child); // true
System.out.println(c instanceof Parent); // true(子类对象是父类实例)
System.out.println(pc instanceof Child); // true(运行时对象是 Child)
System.out.println(p instanceof Child); // false(父类实例不是子类)
System.out.println(null instanceof Parent); // false(null 不是任何类的实例)
}
}
==
运算符
==
运算符,对于基本数据类型,比较的是值,但对于引用数据类型,比较的是地址,也就是用来判断是否是同一个对象。
class A extends B {
}
class B {}
public class Main {
public static void main(String[] args) {
A a = new A();
A b = a;
A c = b;
B bobj = b;
System.out.println(a == b); // true
System.out.println(b == c); // true
System.out.println(bobj == c); // true
}
}
对于基本数据类型,直接比较值是否相同,char
类型被当作数字对待。
int a = 10;
double b = 10.0;
System.out.println(a == b); // true(值相同)
char a = 'A'; // A对应的unicode码是65
int b = 65;
System.out.println(x == y); // true(值相同)
注意事项:
对于String类型,两种初始化的方式所指向的内存是不一样的。
String s1 = new String("Hello");
这种方式会现在堆里面开辟一块内存,在指向常量池中的Hello
,通过这种方式创建相同字符串的两个对象比较结果为false
。
String s2 = "Hello";
这种方式会直接指向常量池中的Hello
,通过这种方式创建相同字符串的两个对象比较结果为true
,因为字符串一样,会指向同样的地址。
但是如果是:
String s1 = new String("hello").intern();
String s2 = "hello";
System.out.println(s1 == s2); // true
s1.intern()
会把 "hello"
放入字符串常量池,并返回池中的引用,所以 s1 == s2
为 true
。
Object
类
toString
方法
toString()
方法用于返回对象的字符串表示形式。
在打印一个对象的时候,实际上隐式地调用了toString()
方法,即输出的是方法的返回值。
默认实现
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
语法
object.toString()
返回值
返回对象的字符串表示形式。
默认返回格式:对象的 class 名称 + @ + hashCode 的十六进制字符串。
示例
public class Main {
public static void main(String[] args) {
A a = new A();
System.out.println(a.toString());
}
}
class A {}
以上程序执行结果为:
A@e580929
重写该方法
默认实现不太有用,通常需要重写,重写该方法的一个示例:
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person("Alice", 25);
System.out.println(p); // 自动调用 p.toString()
}
}
以上程序执行结果为:
Person{name='Alice', age=25}
equals()
方法
equals()
方法用于比较两个对象是否相等。equals() 方法比较两个对象,是判断两个对象引用指向的是同一个对象,即它只是检查两个对象是否指向内存中的同一个地址。
注意:重写该方法,就必须重写hashCode
方法。
默认实现
public boolean equals(Object obj) {
return (this == obj);
}
语法
object.equals(obj)
参数
- obj:要比较的对象。
返回值
如果两个对象相等返回 true,否则返回 false。
示例
class RunoobTest {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
// 判断 obj1 与 obj2 是否相等
// 不同对象,内存地址不同,不相等,返回 false
System.out.println(obj1.equals(obj2)); // false
Object obj3 = obj1;
System.out.println(obj1.equals(obj3)); // true
}
}
以上程序执行结果为:
false
true
重写该方法
默认实现不太有用,通常需要重写,重写时要确保:
- 自反性:
x.equals(x) == true
- 对称性:
x.equals(y) == y.equals(x)
- 传递性:
x.equals(y) && y.equals(z) → x.equals(z)
- 一致性:多次调用
equals()
结果不变(前提是对象未修改) null
比较返回false
:x.equals(null) == false
重写该方法的一个示例:
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 地址相同,直接返回 true
if (obj == null || getClass() != obj.getClass()) return false; // 类型不同,false
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1.equals(p2)); // true
}
}
以上程序执行结果为:
true
hashCode
方法
hashCode()
方法用于获取对象的 hash 值。
默认实现
public int hashCode() {
return System.identityHashCode(this);
}
语法
object.hashCode()
返回值
返回对象哈希值,是一个整数,表示在哈希表中的位置。
示例
class RunoobTest {
public static void main(String[] args) {
Object obj1 = new Object();
System.out.println(obj1.hashCode());
Object obj2 = new Object();
System.out.println(obj2.hashCode());
Object obj3 = new Object();
System.out.println(obj3.hashCode());
}
}
以上程序执行结果为:
225534817
1878246837
929338653
重写该方法
重写该方法要求:
-
如果
equals()
被重写,则hashCode()
也必须重写 -
相同对象
equals()
返回true
,它们的hashCode()
必须相同。 -
不同对象
hashCode()
可以相同,但尽量不相同,减少碰撞,提高哈希效率。
重写该方法的一个示例:
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return name.hashCode() * 31 + age;
}
}
类变量
使用 static
关键字修饰的变量,属于类本身,而不是某个特定对象。类变量在类加载时初始化,仅存在一份。
语法如下:
[访问修饰符] static [final] 数据类型 变量名 [= 初始值];
示例:
class Example {
static int count = 0; // 类变量(静态变量)
}
在访问时,直接通过类名.变量名
直接调用,示例:
public class Main {
public sttic void Main(String[] args) {
System.out.println(Example.count); // 输出0
}
}
注意事项:
- 成员变量和类变量不能同名,否则会报错。
成员方法使用类变量
如果没有定义同名的局部变量,可以直接使用,否则需要通过类名.变量名
的方式访问。
class A {
static final int age = 0;
public void show() {
int age = 1;
System.out.println(A.age);
}
}
类方法
使用 static
修饰的方法,属于类本身,不依赖对象实例。不能使用 this
或 super
关键字,不能直接访问成员变量和调用成员方法,可以创建一个类的实例或接收参数在访问或调用。
语法如下:
[访问修饰符] static [返回值类型] 方法名([参数列表]) {
// 方法体
[return 返回值;]
}
示例:
class Example {
static void printMessage() {
System.out.println("Hello, static method!");
}
}
在调用时,通过类名.方法名(参数)
进行调用,示例:
class Example {
static void printMessage() {
Example.printMessage(); // 输出Hello, static method!
}
}
注意事项:
- 类方法不能和成员方法名称相同并且参数列表相同。
类方法调用类变量
如果没有同名的局部变量,直接使用即可,如果存在同名的局部变量,必需使用类名.变量名
。
class Example {
static int count = 10; // 类变量
void display() {
int count = 5; // 局部变量,遮蔽了类变量
System.out.println(count); // 输出 5(局部变量)
System.out.println(Example.count); // 访问类变量,输出 10
}
}
类方法调用类方法
直接调用类方法即可,或者通过类名.方法名
调用。
public class Main {
public static void main(String[] args) {
Example.methodB();
}
}
class Example {
static void methodA() {
System.out.println("调用 methodA");
}
static void methodB() {
methodA(); // 直接调用同一个类中的 static 方法
Example.methodA();
System.out.println("调用 methodB");
}
}
上述代码执行结果如下:
调用 methodA
调用 methodA
调用 methodB
成员方法调用类方法同理。
main
方法
main
方法是程序的执行入口,具有如下的标准格式:
public static void main(String[] args)
关键字/部分 | 作用 |
---|---|
public |
公开访问,JVM 需要从外部调用该方法。 |
static |
静态方法,无需创建对象即可被 JVM 调用。 |
void |
方法不返回任何值。 |
main |
方法名必须是 main ,区分大小写。 |
String[] args |
传递命令行参数,数组名可以更改(如 String[] arguments 、String... args )。 |
main
方法作为类方法,可以直接访问本类的类变量和调用本类的类方法,但无法直接访问成员变量和调用成员方法,必需创建本类的实例才可以访问或调用。
args
是一个 字符串数组,用于接收命令行参数,运行 Java 程序时可以传递参数,示例:
java Main Hello Java
在 main
方法中可以访问这些参数:
public class Main {
public static void main(String[] args) {
System.out.println("参数个数:" + args.length);
for (String arg : args) {
System.out.println("参数:" + arg);
}
}
}
执行示例中的结果为:
参数个数:2
参数:Hello
参数:Java
代码块
代码块又称为从初始化块,属于类中的成员,是类的一部分。类似于方法,将逻辑语句封装在方法体中,通过{}
包围起来。不同的是,代码块没有方法名,没有返回,没有参数,只有方法体,而且不通过对象或类显式调用,而是在加载类,或创建对象时隐式调用。
语法如下:
[static] {
// 方法体
}
其中static
可选,有static
的叫静态代码块,没有的叫普通代码块。
普通代码块会在创建类的实例时,优先于构造方法执行,每创建一次执行一次。如果只使用静态成员(访问类变量、调用类方法)则不会执行,因为不会创建类的实例。
静态代码块会在加载类时执行一次,且只执行这一次,因为类只会在第一次被使用的时候加载一次,后续在被使用的时候不会再次加载。因为加载类到内存先于创建类的实例,所以静态代码块会优先于普通代码块和构造方法执行。
类的加载情况:
- 创建类的实例的时候
- 创建子类的实例的时候,父类也会被加载
- 使用类的静态成员(类变量、类方法)
示例:
class A {
static int a = 1;
{
System.out.println("普通代码块执行了");
}
static {
System.out.println("静态代码块执行了");
}
public A() {
System.out.println("构造函数执行了");
}
}
public class Main {
public static void main(String[] args) {
int b = A.a;
A a = new A();
}
}
上述代码执行结果如下:
静态代码块执行了 // 加载类执行静态代码块
普通代码块执行了 // 创建类执行普通代码块和构造方法,类已经加载过不在加载,不再执行静态代码块
构造函数执行了
静态代码块只可以访问类变量和调用类方法,而普通代码块还可以调用成员方法和访问成员变量。
静态代码块访问类变量,类变量必需定义在静态代码块之前;普通代码块访问成员变量,成员变量必需定义在普通代码块之前,对类变量无要求,因为类变量的初始化在成员变量之前。
都对调用方法无顺序要求,包括静态方法和类方法,因为方法在变量初始化之前就已经被加载好。此时还可以把静态方法和成员方法的值赋值给类变量或成员变量,对方法和变量的顺序无要求,原因同上。
在类加载的时候,按照代码顺序(自上而下)依次执行静态变量的赋值语句和静态代码块,两者之间没有严格的优先级。在创建类的实例的时候,按照代码顺序(自上而下)依次执行成员变量的赋值语句和普通代码块,两者之间没有严格的优先级。如果变量的值为方法的返回值,就会调用该方法。
class Child {
static {
System.out.println("子类静态代码块");
}
static int a = print("变量a被初始化了");
int b = print("变量b被初始化了");
{
System.out.println("子类普通代码块");
}
public Child() {
System.out.println("子类构造方法");
}
static int print(String str) {
System.out.println(str);
return 0;
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Main 方法执行");
new Child();
}
}
上述代码的执行结果为:
Main 方法执行
子类静态代码块
变量a被初始化了
变量b被初始化了
子类普通代码块
子类构造方法
总结,对于一个类:
- 如果类没有被使用过,也就是没有被加载过,则先加载该类,按照代码顺序(自上而下)依次执行静态变量的赋值语句和静态代码块,如果被加载过,则跳过,不执行静态代码块。
- 如果需要实例化该类,则按照代码顺序(自上而下)依次执行成员变量的赋值语句和普通代码块,最后在执行类的构造方法。
对于继承,如果要创建子类的实例,执行顺序为:自上而下依次加载父类->加载子类->依次创建父类的实例->创建子类的实例。
class Parent {
static { System.out.println("父类静态代码块"); }
{ System.out.println("父类普通代码块"); }
public Parent() { System.out.println("父类构造方法"); }
}
class Child extends Parent {
static { System.out.println("子类静态代码块"); }
{ System.out.println("子类普通代码块"); }
public Child() { System.out.println("子类构造方法"); }
}
public class Main {
public static void main(String[] args) {
System.out.println("Main 方法执行");
new Child();
}
}
上述代码输出结果如下:
Main 方法执行
父类静态代码块
子类静态代码块
父类普通代码块
父类构造方法
子类普通代码块
子类构造方法
如果只调用子类的静态成员,该成员父类没有或子类覆盖了父类的,执行顺序为自上而下依次加载父类->加载子类。
public class Main {
public static void main(String[] args) {
System.out.println(Child.a);
}
}
class Parent {
static {
System.out.println("父类的静态代码块被执行了");
}
static int a = 1;
}
class Child extends Parent {
static {
System.out.println("子类的静态代码块被执行了");
}
static int a = 2;
}
上述代码执行结果为:
父类的静态代码块被执行了
子类的静态代码块被执行了
2
但如果调用的静态成员来自于父类,那么执行顺序为自上而下依次加载父类到静态成员源自的父类,要注意这样并没有加载所有类。
public class Main {
public static void main(String[] args) {
System.out.println(Child.a);
}
}
class A {
static {
System.out.println("A的静态代码块被执行了");
}
}
class B extends A{
static {
System.out.println("B的静态代码块被执行了");
}
static int a = 1;
}
class Parent extends B{
static {
System.out.println("Parent的静态代码块被执行了");
}
}
class Child extends Parent {
static {
System.out.println("Child的静态代码块被执行了");
}
}
上述代码执行结果为:
A的静态代码块被执行了
B的静态代码块被执行了
1
final
关键字
final
修饰类,表示该类不能被继承;final
修饰方法,表示该方法不能被子类重写、隐藏;final
修饰变量,表示该变量的值不可修改,但不能不能修饰构造方法。
final class Parent {
}
class Child extends Parent { // 报错
}
class Parent {
public static final void showA() {
System.out.println("A");
}
public final void showB() {
System.out.println("B");
}
}
class Child extends Parent {
@Override
public final void showB() { // 报错,方法不能被重写
System.out.println("childB");
}
public static void showA() { // 报错,方法不能被隐藏
System.out.println("childA");
}
}
class Parent {
private final int x = 1;
private static final int y = 2;
public void show() {
final int z = 3;
x = 2; // 报错,不能被修改
y = 3; // 报错
z = 4; // 报错
}
}
final
修饰的属性一般叫常量,一般用XX_XX_XX
命名,修饰的属性在定义时必需赋初值,并且为不能再修改,赋值可以在如下位置:
- 定义时,例如:
public final double PI = 3.1415926
- 在构造器中
- 在代码块中
如果修饰的属性是静态的,则初始化的位置只能是定义时或静态代码块中,不能在构造器中赋值。
当 static
final
修饰的变量是 编译期常量(基本数据类型
或 String
),编译器会进行常量折叠优化,直接在字节码中替换变量的值,而不是在运行时访问变量,也就不在加载类。
public class Main {
public static void main(String[] args) {
System.out.println(Parent.PI);
}
}
class Parent {
public static final double PI = 3.14;
static {
System.out.println("静态代码块被执行了");
}
}
上述代码执行结果为:
3.14
但如果变量不是编译器常量,或值在编译期无法确定,比如调用非编译期可知的函数、对象创建、依赖外部输入等,编译器不会进行优化,依然需要加载类。
抽象类
抽象类是不能被实例化的类,专门用来被子类继承,提供基本行为和通用方法。和一般的类相比,抽象类还可以定义抽象方法,就是没有方法体的方法,但不是必需定义抽象方法。
抽象类的语法如下:
abstract [访问修饰符] class 类名 {
}
抽象方法的语法如下:
abstract [访问修饰符] class 类名 {
[访问修饰符] abstract 返回值类型 方法名;
}
[访问修饰符]
不可以是private
,方法也不能用final
和static
修饰,这与重写相违背。
如果一个类定义了抽象方法,那么这个类就必须被声明为抽象类。
抽象类不能被实例化,但是可以访问类变量,调用类方法。