一、java中三种对象初始化的方法
1. 构造函数初始化
构造函数是一种特殊的方法,用于在创建对象时对对象的成员变量进行初始化。它的名称必须与类名相同,并且没有返回类型(包括 void
)。根据参数的不同,构造函数可以分为无参构造函数和有参构造函数。
class Person {
private String name;
private int age;
// 无参构造函数
public Person() {
name = "Unknown";
age = 0;
}
// 有参构造函数
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) {
// 使用无参构造函数创建对象
Person person1 = new Person();
person1.displayInfo();
// 使用有参构造函数创建对象
Person person2 = new Person("Alice", 25);
person2.displayInfo();
}
}
代码解释
Person
类中定义了一个无参构造函数和一个有参构造函数。无参构造函数将name
初始化为"Unknown"
,age
初始化为0
;有参构造函数接收name
和age
作为参数,并将其赋值给对应的成员变量。- 在
Main
类的main
方法中,分别使用无参构造函数和有参构造函数创建了Person
对象,并调用displayInfo
方法显示对象信息。
2. 初始化块初始化
初始化块分为实例初始化块和静态初始化块。实例初始化块在每次创建对象时都会执行,并且在构造函数之前执行;静态初始化块在类加载时执行,并且只执行一次,用于初始化静态成员变量。
class Student {
private String name;
private static int studentCount;
// 实例初始化块
{
name = "New Student";
System.out.println("实例初始化块执行");
}
// 静态初始化块
static {
studentCount = 0;
System.out.println("静态初始化块执行");
}
public Student() {
studentCount++;
System.out.println("构造函数执行");
}
public static void displayStudentCount() {
System.out.println("Student count: " + studentCount);
}
public void displayName() {
System.out.println("Name: " + name);
}
}
public class Main {
public static void main(String[] args) {
// 创建第一个对象
Student student1 = new Student();
student1.displayName();
Student.displayStudentCount();
// 创建第二个对象
Student student2 = new Student();
student2.displayName();
Student.displayStudentCount();
}
}
代码解释
Student
类中定义了一个实例初始化块和一个静态初始化块。实例初始化块将name
初始化为"New Student"
,并输出一条信息;静态初始化块将studentCount
初始化为0
,并输出一条信息。- 在
Main
类的main
方法中,创建了两个Student
对象。可以看到,静态初始化块只在类加载时执行一次,而实例初始化块在每次创建对象时都会执行,并且在构造函数之前执行。
3. 静态工厂方法初始化
静态工厂方法是一种静态方法,用于创建并返回类的对象。与构造函数相比,静态工厂方法具有更灵活的命名和返回类型,可以根据不同的需求创建不同类型的对象。
class Rectangle {
private double length;
private double width;
private Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
// 静态工厂方法
public static Rectangle createSquare(double side) {
return new Rectangle(side, side);
}
public static Rectangle createRectangle(double length, double width) {
return new Rectangle(length, width);
}
public double getArea() {
return length * width;
}
}
public class Main {
public static void main(String[] args) {
// 使用静态工厂方法创建正方形对象
Rectangle square = Rectangle.createSquare(5);
System.out.println("Square area: " + square.getArea());
// 使用静态工厂方法创建矩形对象
Rectangle rectangle = Rectangle.createRectangle(4, 6);
System.out.println("Rectangle area: " + rectangle.getArea());
}
}
二、包
含义
package
语句用于声明当前 Java 源文件中定义的类所属的包。包在 Java 里是一种组织代码的机制,类似于计算机文件系统中的文件夹,能将相关的类和接口组织在一起,方便管理和维护代码。package
语句的格式如下:
package 包名;
其中,“包名” 是一个由点号(.
)分隔的标识符序列,通常采用公司域名的倒序来命名,以确保包名的唯一性。例如:
package com.example.project;
这表明该 Java 源文件中定义的类属于 com.example.project
这个包。
示例
假设你创建了一个简单的自定义类 Person
,并且想把它放在自定义的包 com.example.demo
中,那么 Person.java
文件的内容可能如下:
// 声明该类属于 com.example.demo 包
package com.example.demo;
// 定义 Person 类
public 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);
}
}
注意事项
package
语句位置:package
语句必须是 Java 源文件中的第一条非注释语句,即源文件中不能有任何可执行代码或其他声明在package
语句之前(除了注释和空行)。- 文件目录结构:Java 源文件的目录结构必须与包名对应。例如,
package com.example.demo;
的Person.java
文件应该存放在com/example/demo
目录下,通常是在项目的src
目录下开始的相对路径。
三、构造函数的相互调用
1)有参数的构造方法调用无参数的构造方法
class Person {
private String name;
private int age;
// 无参数的构造方法
public Person() {
this.name = "未知";
this.age = 0;
System.out.println("无参数构造方法被调用");
}
// 有参数的构造方法
public Person(String name, int age) {
// 调用无参数的构造方法
this();
this.name = name;
this.age = age;
System.out.println("有参数构造方法被调用");
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
// 创建 Person 对象,调用有参数的构造方法
Person person = new Person("张三", 20);
System.out.println("姓名: " + person.getName());
System.out.println("年龄: " + person.getAge());
}
}
输出结果:张三,20
注意事项
- 调用位置:
this()
调用必须放在构造方法的第一行,否则会导致编译错误,因为要保证无参数构造方法的初始化逻辑优先执行。 - 避免循环调用:要避免构造方法之间的循环调用,例如有参数构造方法调用无参数构造方法,而无参数构造方法又调用有参数构造方法,这样会导致栈溢出错误。
- 存在性检查:如果要使用
this()
调用无参数构造方法,确保类中确实定义了无参数构造方法,否则会出现编译错误。
2) 无参数的构造方法调用有参数的构造方法
class Book {
private String title;
private double price;
// 有参数的构造方法
public Book(String title, double price) {
this.title = title;
this.price = price;
System.out.println("有参数构造方法被调用");
}
// 无参数的构造方法
public Book() {
// 调用有参数的构造方法
this("默认书名", 0.0);
System.out.println("无参数构造方法被调用");
}
public String getTitle() {
return title;
}
public double getPrice() {
return price;
}
}
public class Main {
public static void main(String[] args) {
// 创建 Book 对象,调用无参数的构造方法
Book book = new Book();
System.out.println("书名: " + book.getTitle());
System.out.println("价格: " + book.getPrice());
}
}
输出结果:默认,0.0
- 调用位置:和有参数构造方法调用无参数构造方法一样,
this()
调用必须放在无参数构造方法的第一行,否则会导致编译错误。 - 避免循环调用:要防止构造方法之间的循环调用,例如无参数构造方法调用有参数构造方法,有参数构造方法又调用无参数构造方法,这会引发栈溢出错误。
- 参数匹配:使用
this()
调用有参数构造方法时,传入的参数类型和数量必须与有参数构造方法的参数列表相匹配,否则会出现编译错误。
四、内部类
(1)实例内部类(成员内部类)
成员内部类是定义在另一个类的内部,但在方法之外的类。它可以访问外部类的所有成员,包括私有成员。
- 访问权限:成员内部类可以访问外部类的所有成员,包括私有成员和静态成员。而外部类要访问成员内部类的成员,需要先创建内部类的对象。
class Outer {
private int outerVar = 10;
class Inner {
void display() {
System.out.println(outerVar);
}
}
public void useInner() {
Inner inner = new Inner();
inner.display();
}
}
1. 如何获取实例内部类的对象
- 创建对象:要创建成员内部类的对象,必须先创建外部类的对象,通过外部类对象来创建内部类对象。格式为
OuterClass.InnerClass inner = outerObject.new InnerClass();
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
- 生命周期:成员内部类的对象依赖于外部类的对象存在,外部类对象销毁时,内部类对象也无法单独存在。
2.实例内部类的非静态方法中包含了一个指向外部类对象的引用
实际上,在这段代码里,内部类 Inner
的 display
方法虽然没有显式地写出指向外部类对象的引用,但它其实是隐式地使用了该引用。
隐式引用机制
在 Java 中,实例内部类(像代码里的 Inner
类)的对象会隐式地持有一个对外部类对象的引用。当在实例内部类的方法中访问外部类的成员时,Java 编译器会自动在背后添加对外部类对象的引用。
System.out.println(outerVar);
看似直接访问了 outerVar
,但实际上 Java 编译器会把它处理成类似于 System.out.println(Outer.this.outerVar);
的形式。这里的 Outer.this
就是指向外部类对象的引用,只不过编译器帮我们隐藏了这个引用的显式书写。
显式引用的情况
class Outer {
private int outerVar = 10;
class Inner {
void display() {
// 显式使用指向外部类对象的引用
System.out.println(Outer.this.outerVar);
}
}
public void useInner() {
Inner inner = new Inner();
inner.display();
}
}
这种显式引用的方式在一些特殊场景下很有用,比如当内部类和外部类存在同名成员时,为了明确指定要访问的是外部类的成员,就需要使用 外部类名.this.成员名
的形式。
实例内部类是依赖于外部类的实例而存在的,也就是说,必须先创建外部类的对象,才能创建实例内部类的对象。
因此,实例内部类的对象会隐式地持有一个对外部类对象的引用,通过这个引用,实例内部类可以访问外部类的所有成员(包括私有成员)。
在实例内部类的非静态方法中,这个引用是默认存在的,可用于调用外部类的成员。
// 外部类
class OuterClass {
private int outerValue = 10;
// 实例内部类
class InnerClass {
public void printOuterValue() {
// 通过外部类类名.this 访问外部类对象的成员
System.out.println("外部类的 outerValue 值为: " + OuterClass.this.outerValue);
}
}
public void createInnerAndCallMethod() {
// 创建实例内部类的对象
InnerClass inner = new InnerClass();
// 调用实例内部类的方法
inner.printOuterValue();
}
}
public class Main {
public static void main(String[] args) {
// 创建外部类的对象
OuterClass outer = new OuterClass();
// 调用外部类的方法,在该方法中创建内部类对象并调用其方法
outer.createInnerAndCallMethod();
}
}
- 外部类
OuterClass
:包含一个私有成员变量outerValue
,其值为 10。 - 实例内部类
InnerClass
:在InnerClass
的非静态方法printOuterValue
中,使用OuterClass.this.outerValue
来访问外部类对象的outerValue
成员。这里的OuterClass.this
就是指向外部类对象的引用,通过它可以明确地引用外部类的成员。
3. 实例内部类当中不能有静态的成员变量,非要定义只能被static final修饰
不能有普通静态成员变量
- 实例内部类的特性:实例内部类是依赖于外部类的实例而存在的,也就是说,必须先创建外部类的对象,才能创建实例内部类的对象。而静态成员变量属于类本身,在类加载时就会被分配内存空间,不依赖于任何对象。
- 逻辑冲突:如果允许实例内部类有普通的静态成员变量,就会出现逻辑上的冲突。因为静态成员变量是类级别的,应该独立于任何实例,但实例内部类又依赖于外部类的实例,这两者之间的依赖关系不匹配。
可以使用 static final
修饰的成员变量
- 常量性质:被
static final
修饰的成员变量是常量,在编译时就会被确定值,并且在整个程序运行过程中不会改变。由于它的值是固定的,不需要依赖于任何实例来初始化,所以可以在实例内部类中定义。
// 外部类
class Outer {
// 实例内部类
class Inner {
// 编译错误,不能有普通的静态成员变量
// static int normalStaticVar = 10;
// 可以使用 static final 修饰的常量
static final int CONSTANT = 20;
public void display() {
System.out.println("常量值: " + CONSTANT);
}
}
}
public class Main {
public static void main(String[] args) {
// 创建外部类对象
Outer outer = new Outer();
// 创建实例内部类对象
Outer.Inner inner = outer.new Inner();
// 调用内部类方法
inner.display();
}
}
(2)静态内部类
这个是为了不创建外部类对象而直接创建内部类对象。
class Outer {
private static int outerStaticVar = 20;
static class StaticInner {
void display() {
System.out.println(outerStaticVar);
}
}
}
- 创建对象:创建静态内部类的对象不需要先创建外部类的对象,可以直接通过
OuterClass.InnerClass inner = new OuterClass.InnerClass();
来创建。
Outer.StaticInner staticInner = new Outer.StaticInner();
- 静态特性:静态内部类中可以定义静态成员和非静态成员,静态成员可以直接通过类名访问。
- 静态内部类当中,不能访问外部类的非静态成员。外部类的非静态成员,需要通过外部类对象的引用才能访问。非要访问:
OuterClass outerClass = new OuterClass(); System.out.println(outerClass.data1);
(3)局部内部类
不咋用 也不好
局部内部类是定义在方法或代码块内部的类,它的作用域仅限于定义它的方法或代码块。
- 访问权限:局部内部类可以访问外部类的成员,以及定义它的方法中的
final
或effectively final
(Java 8 及以后,未被声明为final
但实际上不会被修改的变量)局部变量。
class Outer {
public void method() {
final int localVar = 30;
class LocalInner {
void display() {
System.out.println(localVar);
}
}
LocalInner localInner = new LocalInner();
localInner.display();
}
}
- 作用域:局部内部类只能在定义它的方法或代码块内部使用,出了这个范围就无法访问。所以不需要使用public private等修饰限定。
- 生命周期:局部内部类的生命周期与定义它的方法或代码块的执行周期相关,方法或代码块执行结束后,局部内部类对象也无法再被使用。
(4)匿名内部类
这个等讲了接口再说,
匿名内部类是一种没有名字的局部内部类,通常用于创建只需要使用一次的类的对象。
- 定义和使用:匿名内部类通常用于实现接口或继承抽象类,在创建对象的同时实现接口或抽象类的方法。
interface MyInterface {
void doSomething();
}
class Outer {
public void useAnonymous() {
MyInterface obj = new MyInterface() {
@Override
public void doSomething() {
System.out.println("匿名内部类实现接口方法");
}
};
obj.doSomething();
}
}
- 访问权限:和局部内部类一样,匿名内部类可以访问外部类的成员和定义它的方法中的
final
或effectively final
局部变量。 - 无构造方法:由于匿名内部类没有类名,所以不能定义构造方法,但可以使用实例代码块来进行初始化操作。
五、对象的打印 toString()
在 Java 中,toString()
方法是 Object
类的一个重要方法,所有的类都直接或间接地继承自 Object
类,因此所有类都拥有 toString()
方法。下面从 toString()
方法的默认实现、重写该方法的作用、示例代码以及使用场景等方面详细介绍。
默认实现
Object
类中的 toString()
方法的默认实现返回一个包含类名、@
符号以及对象哈希码的十六进制表示的字符串。其格式通常为 类名@十六进制哈希码
。例如:
public class DefaultToStringExample {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(obj.toString());
}
}
上述代码运行后,输出结果可能类似于 java.lang.Object@1b6d3586
,这种默认输出对于大多数实际应用来说,提供的信息非常有限,不能很好地反映对象的具体状态。
重写 toString()
方法的作用
重写 toString()
方法可以让我们根据类的具体需求,自定义对象的字符串表示形式,从而方便调试、日志记录以及向用户展示对象的信息。通过重写该方法,我们可以将对象的重要属性以可读性强的方式输出。
示例代码
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 重写 toString() 方法
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class OverrideToStringExample {
public static void main(String[] args) {
Person person = new Person("张三", 20);
System.out.println(person);
// 这里直接打印 person 对象,实际上会自动调用 person.toString() 方法
}
}
在上述代码中,Person
类重写了 toString()
方法,将 name
和 age
属性组合成一个更具可读性的字符串。在 main
方法中,当直接打印 person
对象时,会自动调用重写后的 toString()
方法,输出结果为 Person{name='张三', age=20}
。
使用场景
- 调试和日志记录:在开发过程中,当需要查看对象的状态时,重写
toString()
方法可以方便地输出对象的属性值,帮助我们快速定位问题。 - 用户界面展示:在将对象信息展示给用户时,重写
toString()
方法可以提供更友好的输出格式。 - 集合元素输出:当将对象存储在集合中并打印集合时,重写
toString()
方法可以让集合元素的输出更具可读性。例如:
import java.util.ArrayList;
import java.util.List;
public class CollectionToStringExample {
public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
personList.add(new Person("李四", 25));
personList.add(new Person("王五", 30));
System.out.println(personList);
// 由于 Person 类重写了 toString() 方法,这里输出的集合元素信息更清晰
}
}
注意事项
- 遵循契约:重写
toString()
方法时,应尽量遵循toString()
方法的契约,即返回的字符串应该简洁且包含对象的重要信息。 - 性能考虑:如果对象的属性较多或复杂,重写
toString()
方法时要注意性能问题,避免进行过于复杂的计算。