一、包
包(Package)是Java语言中一种管理类和接口等代码结构的机制。它主要用于实现模块化编程,以避免命名冲突和代码重复等问题。
在Java中,每个Java源文件都必须包含一个package语句,这个语句用于指定当前文件中的类所在的包。例如:
package com.example.myproject;
public class MyClass {
// 类的代码
}
在这个例子中,MyClass类被定义在com.example.myproject包中。可以看到,包名的组成是由多个名称组成的,这些名称之间使用点号(.)隔开。通常情况下,包名和文件夹的结构是一致的。
使用包的好处主要有以下几点:
-
避免命名冲突:在不同的包中使用相同的类名不会产生命名冲突,因为每个包都是独立的命名空间。
-
代码重用:在包中定义的类可以被其他包中的类所使用,从而实现代码的重用。
-
访问控制:Java中的访问控制机制是基于包的。类可以使用包访问权限限定符(default)来限制它的可见性范围,只有在同一个包中的类才能访问。
-
组织结构:包可以作为一个逻辑上的组织结构,方便管理和维护代码。例如,可以将相关的类放在同一个包中,从而更容易地进行管理和维护。
在Java中,常用的类库都是按照包的方式组织的。例如,java.lang包中包含了Java语言的核心类,java.util包中包含了一些常用的工具类,javax.servlet包中包含了Servlet API等等。通过包的方式组织类库,可以方便用户按需选择和使用所需要的类和接口。
在IDEA中,创建包类会出现package-info.java
和module-info.java
文件,他们都是Java中用来定义包的文件,但是它们的作用不同。
package-info.java
文件是用来为一个包提供注释和文档信息的,它可以定义在包的顶层目录下,文件名必须是package-info.java
。通常,我们可以在这个文件中添加包级别的注释、变量、方法和内部类等信息,这些信息可以被包中的所有类所使用。在Java 9及以上版本中,还可以在这个文件中声明@Deprecated
、@SuppressWarnings
等注解。
module-info.java
文件是用来定义Java 9及以上版本中引入的模块化系统的,它也可以定义在包的顶层目录下,文件名必须是module-info.java
。在这个文件中,我们可以声明该模块所依赖的其他模块、导出的包、开放的包、提供的服务、使用的服务等信息。这个文件的作用是将多个包组成一个可重用的、独立的、具有良好封装性的单元。
介绍几个常用包:
-
java.lang包:这是Java中最基本的包,包含了Java语言的基本类和接口,如String、Integer、Boolean等。
-
java.util包:这是Java中常用的工具类包,包含了许多集合类、日期时间类、随机数类等。
-
java.io包:这是Java中输入输出类的包,包含了读写文件、网络传输、数据流等操作的类和接口。
-
java.net包:这是Java中用于网络编程的包,包含了许多网络通信的类和接口,如Socket、ServerSocket、URL等。
-
java.awt包和javax.swing包:这两个包提供了Java图形界面编程的类和接口,可以用于创建窗口、菜单、按钮等GUI组件。
-
java.sql包:这是Java中用于访问关系型数据库的包,包含了JDBC API接口和一些实现类,如Connection、Statement、ResultSet等。
这些包都是Java开发中常用的基础包,熟练掌握它们的使用方法对于开发Java程序是非常重要的。他们们是Java语言中预定义的标准包,可以直接在代码中使用。使用方法如下:
-
声明包名:在Java源文件中,使用
package
关键字来声明包名,例如:package java.util;
。该语句通常放在源文件的第一行。 -
导入包:在Java源文件中,使用
import
关键字来导入包中的类、接口、枚举等,例如:import java.util.ArrayList;
。可以使用通配符*
来导入包中的所有类,例如:import java.util.*;
。
需要注意的是,Java语言中不同的包可能存在同名的类,因此在导入时需要注意使用全限定名,避免出现冲突。例如:java.util.Date
和java.sql.Date
。
二、内部类
内部类是定义在另一个类内部的类。可以看做是外部类的一个成员,它可以访问外部类的所有成员,包括私有成员。Java 中,内部类的定义方式如下:
Java 中的内部类可以分为以下几种类型:
- 普通内部类(成员内部类):定义在另一个类的内部,并且没有使用
static
修饰。 - 静态内部类:定义在另一个类的内部,并且使用
static
修饰。 - 方法内部类(局部内部类):定义在方法内部,只能在该方法中使用。
- 匿名内部类:没有类名的内部类,通常用于创建一次性的、只需要简单实现某个接口或抽象类的类的对象。
内部类的使用场景主要包括以下几种:
- 封装:内部类可以访问外部类的所有成员,包括私有成员,因此可以实现更加严格的封装。
- 实现多重继承:Java 不支持多重继承,但是内部类可以实现多重继承的效果。
- 回调函数:内部类通常用于实现回调函数。
例1,成员内部类:
成员内部类是指定义在另一个类中的类。使用成员内部类可以访问外部类的所有成员(包括私有成员),并且可以通过外部类的实例来创建成员内部类的实例。使用方法如下:
-
private int outerField;
定义了外部类的一个私有成员变量outerField
。 -
public class InnerClass { ... }
定义了一个内部类InnerClass
,其中包含一个私有成员变量innerField
和一个构造函数public InnerClass(int innerField)
。 -
public void printFields() { ... }
定义了一个方法printFields()
,它打印了外部类的outerField
和内部类的innerField
。 -
public void createInnerClass() { ... }
定义了一个方法createInnerClass()
,它创建一个内部类对象InnerClass
,并调用它的printFields()
方法。
例2,静态内部类
静态内部类是指在另一个类中使用 static 修饰的内部类。与成员内部类不同的是,静态内部类只能访问外部类的静态成员,不能访问外部类的非静态成员。使用方法如下:
这段代码定义了一个名为 OuterClass
的外部类,其中包含一个名为 outerStaticField
的静态变量和一个名为 createInnerClass
的方法。
该类还定义了一个名为 InnerClass
的静态内部类。这个内部类包含一个名为 innerField
的实例变量和一个名为 printFields
的方法,用于打印外部类的静态变量和内部类的实例变量。
在 createInnerClass
方法中,我们实例化 InnerClass
类并调用其 printFields
方法来打印外部类的静态变量和内部类的实例变量。
需要注意的是,静态内部类与外部类之间的联系比非静态内部类更加松散。静态内部类可以独立于外部类而存在,而非静态内部类必须寄托于外部类的实例。另外,静态内部类不能访问外部类的实例变量,但可以访问外部类的静态变量。
例3,局部内部类:
局部内部类是定义在方法内部的类。它与其他类型的内部类不同的是,它的作用域仅限于定义它的方法内部,所以它不能被访问和实例化。使用方法如下:
这段代码定义了一个外部类 OuterClass
,其中包含一个方法 createInnerClass
,该方法中定义了一个局部内部类 InnerClass
。
在 createInnerClass
方法中,首先定义了一个局部内部类 InnerClass
,然后创建了一个 InnerClass
的对象 inner
,并调用了 inner
的方法 printFields()
。
InnerClass
类有一个带有一个参数的构造函数,用于初始化其成员变量 innerField
。InnerClass
类还有一个方法 printFields()
,用于输出 innerField
的值。
需要注意的是,局部内部类只能在定义该类的方法或语句块中使用。在这个例子中,InnerClass
类只能在 createInnerClass
方法中被使用。
例4,匿名内部类:
匿名内部类是一种没有名字的内部类,通常用于创建实现某个接口或继承某个类的对象。使用方法如下:
这段代码定义了一个名为 OuterClass
的类,并在其中定义了一个方法 createInnerClass
。该方法通过匿名内部类创建了一个 Runnable
实例,并将其作为参数传递给 Thread
类的构造函数,最终创建了一个新的线程并启动它。
具体解释如下:
- 首先,通过
Runnable r = new Runnable() {...}
创建了一个匿名内部类的实例,这个匿名内部类实现了Runnable
接口,并实现了run
方法,该方法会在新线程中运行。 - 然后,通过
Thread t = new Thread(r)
创建了一个新的线程,其中r
是刚刚创建的Runnable
实例。 - 最后,通过
t.start()
启动了新线程,使其运行刚刚创建的run
方法中的代码,从而打印出 "Hello, world!"。
需要注意的是,匿名内部类只能用于一次性的类定义,因此在该代码中只能使用一次。而如果需要多次使用相同的代码块,建议将其定义为具名的内部类或独立的类。
例5,多重继承:
多重继承是指一个类继承了多个父类。在 Java 中,一个类只能直接继承一个父类,这是因为 Java 只支持单一继承,即一个类只能继承自一个直接父类。但是 Java 中可以通过接口来实现多重继承的效果,一个类可以实现多个接口,从而具有了多个接口的功能。
需要注意的是,多重继承可能导致的问题包括菱形继承问题和命名冲突问题。菱形继承问题是指一个子类继承了两个不同的父类,这两个父类又共同继承了同一个父类,形成了一个菱形的继承结构,这会导致方法调用的不确定性和冗余。Java 中通过接口来实现多重继承,避免了菱形继承问题。命名冲突问题是指两个不同的父类或接口中有相同的方法名或属性名,这会导致编译器无法判断使用哪个父类或接口的方法或属性,因此必须手动指定使用哪个方法或属性。为了避免命名冲突问题,可以使用接口的默认方法和静态方法来实现功能,而不是继承父类的方法和属性。
PS:假设有一个Animal类和一个CanRun接口,Animal类实现了CanRun接口,同时还有一个Person类继承了Animal类,那么Person类也就同时继承了CanRun接口的方法。
interface CanRun {
void run();
}
class Animal implements CanRun {
public void run() {
System.out.println("Animal is running");
}
}
class Person extends Animal {
// Person继承了Animal的run方法
// 同时也继承了CanRun接口的run方法
}
public class Example {
public static void main(String[] args) {
Person p = new Person();
p.run(); // Animal is running
((CanRun)p).run(); // Animal is running
}
}
例6,回调函数:(例子很难,明白意思就行)
l回调函数是一种常见的编程模式,通常用于异步编程中。它可以让调用者在发起一个异步操作后,不必等待操作完成,而是通过传入一个函数或方法的引用作为参数,在操作完成后由被调用者来执行该函数或方法。
举一个在 Android 应用开发中常用的回调函数例子。在 Android 中,常常需要等待一些耗时操作完成后再执行一些逻辑,例如网络请求、数据查询等,而这些耗时操作往往需要在后台线程中进行,否则会阻塞主线程。为了实现异步执行,Android 中通常会使用回调函数来实现异步回调。
例如,在进行网络请求时,通常会使用一个网络请求库,例如 Retrofit。Retrofit 通常会使用一个回调函数来处理网络请求的结果,例如下面的示例代码:
在这个回调函数中,onResponse()
方法会在网络请求成功时被调用,onFailure()
方法会在网络请求失败时被调用。开发者可以在实现回调函数时提供自定义的逻辑来处理网络请求的结果。例如,下面是一个使用 Retrofit 发起网络请求并处理结果的示例:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
Call<List<Repo>> repos = service.listRepos("octocat");
repos.enqueue(new Callback<List<Repo>>() {
@Override
public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
List<Repo> repoList = response.body();
// 处理网络请求成功的结果
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
// 处理网络请求失败的结果
}
});
在这个示例中,repos.enqueue()
方法会在后台线程中发起网络请求,并将结果通过回调函数的方式传递给开发者自定义的逻辑。如果网络请求成功,开发者实现的 onResponse()
方法会被调用,如果网络请求失败,开发者实现的 onFailure()
方法会被调用。开发者可以在这两个方法中实现自定义的逻辑来处理网络请求的结果。