在Java中,类与对象是面向对象编程(OOP)的核心概念。那面向对象又是什么呢。
一、面向对象和面向过程
1、面向对象
面向对象(Object-oriented)是一种程序设计的方法和编程范式,它以对象作为程序的基本单位,通过封装、继承和多态等概念来组织和实现程序逻辑。面向对象的编程思想强调将问题分解为对象的集合,每个对象具有自己的状态(属性)和行为(方法),并通过相互之间的消息传递来实现协作和交互。
面向对象的三大基本特征:
封装(Encapsulation):
封装是将数据(属性)和行为(方法)封装在对象内部,隐藏对象的实现细节,只暴露必要的接口,实现数据的保护和模块化。
继承(Inheritance):
继承是面向对象编程的一种机制,通过继承,一个类(子类)可以继承另一个类(父类)的属性和方法,促进代码重用和扩展。
多态(Polymorphism):
多态允许同一个接口有多个实现,通过动态绑定在运行时决定具体调用哪种实现,增强代码的灵活性和可扩展性。
2、面向过程
面向过程编程(Procedural Programming)是一种以过程为中心的编程方式,将程序视为一系列的步骤或过程的集合。它关注如何完成任务,通过编写一系列的函数来实现功能,函数接受输入,执行一系列操作,并返回输出。面向过程编程强调算法和步骤的顺序,逐步解决问题。
- 过程中心:程序由过程(函数)组成,按照执行顺序依次调用。
- 模块化:通过函数将代码分解为模块,但函数之间可能存在紧密耦合。
3、示例
我们可以用一个例子更加生动的介绍一下这两者的区别:
面向对象
例如我们使用豆浆机打豆浆,如果在免面向对象的情况下,豆浆机、水、大豆和豆浆都是对象:
class Soy {//...
}class Water {//...
}class Soymilk {//...
}class SoymilkMaker {//...public static void addWater() {//...}public static void addSoy() {//...}public static Soymilk makeSoymilk() {//...}
}
在这个面向对象的例子中,SoymilkMaker
类封装了加水、加大豆和制作豆浆的操作。我们通过创建 SoymilkMaker
对象并调用它的方法来实现整个过程。数据(water
和 soy
)和行为(addWater
和 addSoy
方法)都被封装在对象内部。
面向过程
在面向过程编程中,我们将任务分解为不同的函数,每个函数执行特定的操作。
void AddWater(Water) {//...
}void AddSoy(Soy) {//...
}Soymilk MakeSoymilk(Water, Soy) {AddWater();AddSoy();//...
}
每一个函数都有自己的任务。在这个面向过程的例子中,我们通过一系列函数调用来完成制作豆浆的过程。这些函数分别处理加水、加大豆和制作豆浆的任务。
二、类的定义和实例化
1、类和对象的关系
1)类(Class)
类可以看作是对象的模板或蓝图。它定义了对象的属性(数据)和行为(方法)。类本身是抽象的,不占用内存。
2)对象(Object)
对象是类的一个具体实例。每个对象都有独立的属性和方法。当一个类被实例化时,会在内存中创建一个对象,这个对象包含了类中定义的所有属性和方法。当对象被创建时,系统会在内存中为其分配空间来存储属性值。不同对象的属性存储在不同的位置,不会互相干扰。
3)之间的关系
类就是一种数据类型,它只能说是一种模版,例如 int 和 double 等类型。而对象是类的实例,就像是使用类这个数据类型创建了一个变量,就像 int a; 这里的 a 就像一个实例一样,它在内存中是有被分配空间的。
4)图解
2、类的定义
类的声明使用class
关键字,后跟类名,通常遵循大驼峰命名法(即每个单词的首字母大写)。类名紧跟着一对花括号,花括号内包含类的成员(属性和方法)。
// 类的定义模板
访问修饰符 class 类名 {// 成员变量(属性)访问修饰符 数据类型 变量名;// 构造方法,构造方法不需要返回类型访问修饰符 类名(参数列表) {// 构造方法的实现}// 成员方法访问修饰符 返回类型 方法名(参数列表) {// 方法的实现}
}
例如:
class Person {//人的属性private String name;//名字private int age;//年龄//构造方法public Person(String name,int age) {this.name = name;this.age = age;}//公共方法显示信息public void printInfo() {System.out.println("name:" + this.name + "age:" + this.age);}// 公有的 getter 方法public String getName() {return name;}public int getAge() {return age;}//...
}
3、类的实例化
1)使用 new 关键字创建实例
当我们有了一个类,我们可以将这个类实例化,也就是通过这个类创建一个对象(实例)。
下面是最常见的实例化方式,使用 new
关键字来调用类的构造方法创建一个类的实例。
public class Test {public static void main(String[] args) {Person person1 = new Person("张三",18);Person person2 = new Person("李四",19);System.out.println("person1 " + person1.getName() + " " + person1.getAge());System.out.println("person2 " + person2.getName() + " " + person2.getAge());}
}class Person {//人的属性private String name;//名字private int age;//年龄//构造方法public Person(String name,int age) {this.name = name;this.age = age;}//公共方法显示信息public void printInfo() {System.out.println("name:" + this.name + "age:" + this.age);}// 公有的 getter 方法public String getName() {return name;}public int getAge() {return age;}//...
}
类可以定义多个构造方法,通过传入不同的参数来实例化对象。也就是构造方法重载。
// 定义一个类
class Person {private String name;private int age;// 无参构造方法public Person() {this.name = "Unknown";this.age = 0;}// 参数为String和int的构造方法public Person(String name, int age) {this.name = name;this.age = age;}public void displayInfo() {System.out.println("Name: " + name + ", Age: " + age);}
}public class Main {public static void main(String[] args) {// 使用 new 关键字使用无参构造方法实例化类Person person1 = new Person();person1.displayInfo();// 使用 new 关键字使用参数为String和int的构造方法实例化类Person person2 = new Person("张三", 18);person2.displayInfo();}
}
还可以使用工厂方法模式。工厂方法模式是通过定义一个工厂类来封装对象的实例化过程。
// 定义一个类
class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}public void displayInfo() {System.out.println("Name: " + name + ", Age: " + age);}
}// 定义一个工厂类
class PersonFactory {public static Person createPerson(String name, int age) {return new Person(name, age);}
}public class Main {public static void main(String[] args) {// 使用工厂方法实例化类Person person = PersonFactory.createPerson("李四", 19);person.displayInfo();}
}
2)使用反射机制
反射机制允许在运行时动态地获取类的信息,并实例化类。
public class Main {public static void main(String[] args) {try {// 使用反射机制实例化类Class<?> cls = Class.forName("Person");Person person = (Person) cls.getDeclaredConstructor().newInstance();person.displayInfo();} catch (Exception e) {e.printStackTrace();}}
}
3)单例模式
单例模式确保一个类只有一个实例,并提供全局访问点。
// 定义一个类
class Singleton {private static Singleton instance = null;private Singleton() {// 私有构造方法,防止外部实例化}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}public void displayInfo() {System.out.println("This is a singleton instance.");}
}public class Main {public static void main(String[] args) {// 使用单例模式实例化类Singleton singleton = Singleton.getInstance();singleton.displayInfo();}
}
以上几种实例化类的方法各有其使用场景:
- 使用关键字
new
:最常见、最直接的实例化方式。 - 使用构造方法重载:允许灵活地通过不同参数创建实例。
- 使用反射机制:用于需要动态加载和实例化类的场景,如插件框架。
- 使用工厂方法模式:封装实例化过程,符合面向对象设计原则。
- 使用单例模式:保证类的唯一实例,适用于需要全局唯一对象的场景。
三、对象在内存中的存储
1、对象在内存中存储情况
下面我们对对象创建后再内存中的情况进行分析。
这里用以下对象实例举例:
Fish goldenFish = new Fish("goldenFish",0.25);class Fish {public String breed;public double weight;public Fish(String breed,int weight) {this.breed = breed;this.weight = weight;}public void swim() {//...}
}
这里提前介绍一下字符串常量池。在Java中,字符串的内存管理有一些特别之处。具体来说,字符串字面量(如“goldenFish”) 通常会存储在字符串常量池(String Constant Pool)中,但这会根据具体的代码实现和运行时条件有所不同。
字符串常量池是为了提高性能和减少内存消耗而设计的。它是方法区的一部分,用于存储在编译时确定的字符串常量和运行时创建的字符串常量。
当我们在代码中使用字符串字面量时,Java编译器会自动将这些字面量放入字符串常量池。例如:
String str1 = "张三"; String str2 = "张三";
在这种情况下,
str1
和str2
都将引用同一个字符串常量池中的 "张三" 对象,因此str1 == str2
将返回true
。因为 String 也是类,所以 str1 和 str2 都是对象引用变量,所以其中存储的都是引用,这里的字符串常量 "张三" 会被放入字符串常量池,然后 str1 和 str2 中存储的都是这一个字符串常量池中的 "张三" 的引用。所以它们中存储的引用是相等的。字符串对象可以通过两种方式创建:
使用字符串字面量:
当使用字符串字面量(例如
String str = "张三";
)时,字符串会被放入字符串常量池。如果常量池中已经存在相同内容的字符串,则直接返回该字符串的引用。然后 str 中存储的就是字符串常量池中的 "张三" 的引用。使用
new
关键字:当使用
new
关键字创建字符串对象时(例如String str = new String("张三");
),即使字符串常量池中已经存在相同内容的字符串,依然会在堆中创建一个新的字符串对象。//str1 引用的是字符串常量池中的 "张三" String str1 = "张三"; //str2 引用的是堆中的字符串对象 "张三" String str2 = new String("张三");
因此这里的 str1 == str2 将返回 false。因为它们存储的引用是不同的。
如果我们使用一个常量字符串 "张三" 用来初始化一个 String 类型的对象,那么编译器会首先检查字符串 "张三" 是否在字符串常量池中。如果存在,则直接使用该引用;否则,将其添加到常量池中,然后再使用其引用。
在介绍了这个知识点后,我们可以接着分析我们上面的代码。
Fish goldenFish = new Fish("goldenFish",0.25);
显然我们这里使用的是常量字符串作为参数传入构造函数的,所以这里的字符串就应当在字符串常量池。
然后对于 goldenFish 这个对象引用变量,它存储的就是 goldenFish 这个对象的引用, goldenFish 这个对象的一些属性就存储在堆区。但是对于 goldenFish 的 breed 属性,是一个字符串类型,它也是一个对象,又比较特殊,这里使用的是常量字符串,所以这里的 breed 作为 String 类型对象的引用变量,存储的引用是字符串常量池中的常量字符串的引用。
对于 goldenFish 这个对象的其他属性 weight 就是直接存储在堆区的,因为它是基础类型之一的 double。
对于 goldenFish 的一些构造方法和普通方法的字节码均存储在方法区中。当类被加载时,它们会被解析并存储在方法区。方法的实际执行代码在方法区中,而方法内的局部变量和方法参数在被调用时存储在栈中(与 C 语言中的函数栈帧开辟与销毁类似)。
2、对象存储图解
3、具体步骤与内存分配
类加载:
- JVM检查
Fish
类是否已经加载。如果没有加载,会加载Fish
类,并在方法区中存储类信息,包括类的构造方法和其他方法的字节码。
对象创建:
- JVM在堆中为新的
Fish
对象分配内存。
成员变量初始化:
- 对
Fish
对象的成员变量进行初始化。 breed
引用字符串常量池中的字符串"goldenFish"
。weight
直接存储在对象的内存中,因为它是基本类型double
。
构造方法调用:
- JVM调用
Fish
类的构造方法,并将参数"goldenFish"
和0.25
传递给构造方法。 - 构造方法对对象进行初始化,将参数值赋给对象的成员变量。
4、类和对象在内存中的分配机制
在我们创建一个对象时,就像下面这样:
Fish fish = new Fish();class Fish {public String breed;public double weight;public void swim() {//...}
}
这里的 fish 这个变量实际上是一个对象引用变量,其中存储的是真实的在堆区的对象的引用。我们上面就已经提到了。
如果我们将这个对象引用变量的值赋值个另一个对象引用变量的话,就可以通过另一个对象引用变量访问这个对象了。就像下面:
Fish fish = new Fish();
//声明一个新的对象引用变量
Fish fish2;
//将之前的对象引用变量的值赋值给新的对象引用变量
fish2 = fish;
然后 fish2 和 fish 都指向同一个对象了。
四、类的属性(成员变量 / 字段)
属性是类的一个组成部分,属性可以是任意数据类型,可以是基础数据类型也可以是引用数据类型。
1、属性的定义
属性的定义与变量的定义相似,但是属性的定义最前面会多一个访问修饰符。
变量定义:
int num;
属性定义:
访问修饰符 int num;
访问修饰符:
在Java中,访问修饰符(Access Modifiers)用于控制类、方法和属性(字段)的可见性。它们决定了其他类是否能够访问特定的类成员(属性或方法)。Java中有四种主要的访问修饰符:
public
protected
- (什么都不写时的访问级别为默认)
private
修饰符 当前类 同一个包 子类(不同包) 其他包 public
√ √ √ √ protected
√ √ √ × 默认
√ √ × × private
√ × × ×
2、属性的默认值
属性定义后不初始化,会有默认初始值,与数组元素的默认初始值同样。
int | short | byte | long | float | double | char | boolean | String |
0 | 0 | 0 | 0L | 0.0f | 0.0 | \u0000 | false | null |
我们可以使用代码验证:
public class Test {public static void main(String[] args) {Fish fish = new Fish();System.out.println("breed " + fish.breed);System.out.println("age " + fish.age);System.out.println("weight " + fish.weight);}
}class Fish {String breed;int age;float weight;//...
}
运行结果:
3、对象访问其属性
在Java中,对象通过点(.
)操作符来访问其属性和方法。我们上面已经给出了很多例子了。
五、类的成员方法
类的成员方法简称方法。在Java中,类的成员方法(也称为实例方法或成员函数)是定义在类内部的函数,用于实现类的对象的行为。成员方法可以访问和修改类的属性,并可以与其他方法进行交互。
1、方法的定义
成员方法可以包含以下几部分:
- 访问修饰符:控制方法的可见性(如
public
、private
、protected
、默认)。 - 返回类型:定义方法返回的值类型,如果不返回任何值,使用
void
。对于构造方法比较特殊,构造方法不用写返回类型。 - 方法名:标识方法的名称。
- 参数列表:方法可以接受的输入参数,包含参数的类型和名称。
- 方法体:方法的具体实现代码。
访问修饰符 返回类型 方法名(参数列表) {//方法体
}
例如:
class Person {String name;int age;//说话方法public void speak(String sentences) {System.out.println(sentences);}//介绍方法public void introduce() {System.out.println("你好,我是" + this.name);}
}
这里的说话方法和介绍方法就是这个类的方法。
2、方法的种类
1)构造方法(构造器)
构造方法(Constructor)是用于创建和初始化类对象的一种特殊方法。在Java中,构造方法的名称必须与类名相同,并且没有返回类型(即使是void
也不能写)。构造方法在创建对象时自动调用,它可以用于初始化对象的属性或执行任何需要在对象创建时完成的操作。一个类可以有多个构造方法,只要它们的参数列表不同。
构造方法在创建对象时被自动调用。使用new
关键字创建对象时,系统会根据传递的参数选择合适的构造方法来调用。
public class Test {public static void main(String[] args) {Person person = new Person("张三",19);}
}class Person {String name;int age;public Person(String name,int age) {this.name = name;this.age = age;}//...
}
这里的语句:
Person person = new Person("张三",19);
就调用了构造方法,我们传入了两个参数,刚好有一个两个参数的构造方法,调用的刚好就是那一个。
如果一个类没有显式地定义任何构造方法,Java编译器会自动为该类生成一个默认的无参构造方法。这个默认的构造方法是public
的,且方法体为空。例如,对于一个类Person
,如果没有定义任何构造方法,编译器会自动生成如下构造方法:
public class Person {// 编译器自动生成的无参构造方法public Person() {}
}
如果你定义了一个构造方法,那么编译器不会再自动生成无参构造方法。
2)一般自定义方法
一般自定义方法,也被称为实例方法,是属于类的实例的,也就是属于对象的。它们定义在类内部,可以访问实例变量和其他实例方法。
class Person {String name;int age;//说话方法public void speak(String sentences) {System.out.println(sentences);}//介绍方法public void introduce() {System.out.println("你好,我是" + this.name);}
}
3)静态方法
静态方法属于类,而不是类的实例。静态方法使用static
关键字定义,可以直接通过类名调用,而不需要创建对象。
public class MathUtils {// 静态方法public static int square(int x) {return x * x;}
}
3、方法的调用
1)构造方法调用
构造方法在创建对象时自动调用。构造方法在创建对象时被自动调用。使用new
关键字创建对象时,系统会根据传递的参数选择合适的构造方法来调用。
public class Test {public static void main(String[] args) {Person person = new Person("张三",19);}
}class Person {String name;int age;public Person(String name,int age) {this.name = name;this.age = age;}//...
}
这里的语句:
Person person = new Person("张三",19);
就调用了构造方法,我们传入了两个参数,刚好有一个两个参数的构造方法,调用的刚好就是那一个。
我们这里只有一个两个参数构造方法,如果我们在创建对象时不传入参数,或者传入其他数量的参数,就会因为找不到适当的构造方法而报错:
构造方法的重载:
类可以有多个构造方法,称为构造方法的重载(Overloading)。这些构造方法具有相同的名称(即类名),但参数列表不同。
public class Test {public static void main(String[] args) {Person person1 = new Person("张三",19);//调用两个参数的构造方法Person person2 = new Person();//调用无参数的构造方法}
}class Person {String name;int age;//无参数的构造方法public Person() {this.name = "未知";this.age = 0;}//两个参数的构造方法public Person(String name,int age) {this.name = name;this.age = age;}//...
}
2)一般自定义方法调用
一般自定义方法要创建对象,然后通过对象调用相应的方法。
public class Test {public static void main(String[] args) {Person person = new Person();person.name = "张三";person.age = 19;person.introduce();//调用一般自定义方法}
}class Person {String name;int age;//说话方法public void speak(String sentences) {System.out.println(sentences);}//介绍方法public void introduce() {System.out.println("你好,我是" + this.name);}
}
运行结果:
3)静态方法调用
静态方法可以通过类名调用。
public class MathUtils {// 静态方法public static int square(int x) {return x * x;}public static void main(String[] args) {// 调用静态方法System.out.println(MathUtils.square(5)); // 输出:25}
}
对于 square 就是一个静态方法,它可以直接通过类名进行调用。
4)直接调用
在同一个类中,可以直接通过方法名进行调用。
public class Example {public static void main(String[] args) {sayHello();}public static void sayHello() {System.out.println("Hello, World!");}
}
5)方法的重载(Overloading)
在同一个类中,可以定义多个方法(包括构造方法,构造方法也可以重载),它们具有相同的名称但参数列表不同,这称为方法重载。对于这种情况调用时传入不同的参数,使用的方法是不同的。
public class Calculator {// 方法重载:同名方法,参数不同public int add(int a, int b) {return a + b;}public double add(double a, double b) {return a + b;}public int add(int a, int b, int c) {return a + b + c;}public static void main(String[] args) {Calculator calc = new Calculator();System.out.println(calc.add(1, 2)); // 调用第一个add方法System.out.println(calc.add(1.5, 2.5)); // 调用第二个add方法System.out.println(calc.add(1, 2, 3)); // 调用第三个add方法}
}