7.2.1 作用域
在Java中,我们经常看到public
、protected
、private
这些修饰符。在Java中,这些修饰符可以用来限定访问作用域。
public
定义为public
的class
、interface
可以被其他任何类访问:
//package abc;
public class Hello {
public void hi() {
}
}
上面的Hello
是public
,因此,可以被其他包的类访问:
//package xyz;
class Main {
void foo() {
// Main可以访问Hello
Hello h = new Hello();
}
}
定义为public
的field
、method
可以被其他类访问,前提是首先有访问class
的权限:
//package abc;
public class Hello {
public void hi() {
}
}
上面的hi()
方法是public
,可以被其他类调用,前提是首先要能访问Hello
类:
//package xyz;
class Main {
void foo() {
Hello h = new Hello();
h.hi();
}
}
private
定义为private
的field
、method
无法被其他类访问:
//package abc;
public class Hello {
// 不能被其他类调用:
private void hi() {
}
public void hello() {
this.hi();
}
}
实际上,确切地说,private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法:
//package abc;
public class Hello {
public void hello() {
this.hi();
}
private void hi() {
}
}
由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:
// private
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}
// private方法:
private static void hello() {
System.out.println("private hello!");
}
// 静态内部类:
static class Inner {
public void hi() {
Main.hello();
}
}
}
定义在一个class内部的class称为嵌套类(nested class),Java支持好几种嵌套类。
protected
protected作用于继承关系(下次课会讲到继承,这里先了解下)。定义为protected的字段和方法可以被子类访问,以及子类的子类:
//package abc;
public class Hello {
// protected方法:
protected void hi() {
}
}
上面的protected方法可以被继承的类访问:
//package xyz;
class Main extends Hello { //extends 继承关键字
void foo() {
// 可以访问protected方法:
hi();
}
}
package
最后,包作用域是指一个类允许访问同一个package
的没有public
、private
修饰的class,以及没有public
、protected
、private
修饰的字段和方法。
//package abc;
// package权限的类:
class Hello {
// package权限的方法:
void hi() {
}
}
只要在同一个包,就可以访问package权限的class、field和method:
//package abc;
class Main {
void foo() {
// 可以访问package权限的类:
Hello h = new Hello();
// 可以调用package权限的方法:
h.hi();
}
}
注意,包名必须完全一致,包没有父子关系,com.apache和com.apache.abc是不同的包。
局部变量
在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
//package abc;
public class Hello {
void hi(String name) { // 1
String s = name.toLowerCase(); // 2
int len = s.length(); // 3
if (len < 10) { // 4
int p = 10 - len; // 5
for (int i=0; i<10; i++) { // 6
System.out.println(); // 7
} // 8
} // 9
} // 10
}
我们观察上面的hi()方法代码:
- 方法参数name是局部变量,它的作用域是整个方法,即1 ~ 10;
- 变量s的作用域是定义处到方法结束,即2 ~ 10;
- 变量len的作用域是定义处到方法结束,即3 ~ 10;
- 变量p的作用域是定义处到if块结束,即5 ~ 9;
- 变量i的作用域是for循环,即6 ~ 8。
使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量。
final
Java还提供了一个final修饰符。final与访问权限不冲突,它有很多作用。
用final修饰class可以阻止被继承:
//package abc;
// 无法被继承:
public final class Hello {
private int n = 0;
protected void hi(int t) {
long i = t;
}
}
用final修饰method可以阻止被子类覆写:
//package abc;
public class Hello {
// 无法被覆写:
protected final void hi() {
}
}
用final修饰field可以阻止被重新赋值:
//package abc;
public class Hello {
private final int n = 0;
protected void hi() {
this.n = 1; // error!
}
}
| this.n = 1; // error!
无法为最终变量n分配值
用final修饰局部变量可以阻止被重新赋值:
//package abc;
public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}
| t = 1; // error!
不能分配最终参数t
最佳实践
-
如果不确定是否需要
public
,就不声明为public
,即尽可能少地暴露对外的字段和方法。 -
把方法定义为
package
权限有助于测试,因为测试类和被测试类只要位于同一个package
,测试代码就可以访问被测试类的package
权限方法。 -
一个
.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。
小结
Java内建的访问权限包括public、protected、private和package权限;
Java在方法内部定义的变量是局部变量,局部变量的作用域从变量声明开始,到一个块结束;
final修饰符不是访问权限,它可以修饰class、field和method;
一个.java文件只能包含一个public类,但可以包含多个非public类。
7.2.2内部类
在Java程序中,通常情况下,我们把不同的类组织在不同的包下面,对于一个包下面的类来说,它们是在同一层次,没有父子关系:
java.lang
├── Math
├── Runnable
├── String
└── ...
还有一种类,它被定义在另一个类的内部,所以称为内部类(Nested Class)。Java的内部类分为好几种,通常情况用得不多,但也需要了解它们是如何使用的。
Inner Class
如果一个类定义在另一个类的内部,这个类就是Inner Class:
class Outer {
class Inner {
// 定义了一个Inner Class
}
}
上述定义的Outer是一个普通类,而Inner是一个Inner Class,它与普通类有个最大的不同,就是Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例。示例代码如下:
// inner class
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested"); // 实例化一个Outer
Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
inner.hello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}
Main.main(null);
Hello, Nested
观察上述代码,要实例化一个Inner,我们必须首先创建一个Outer的实例,然后,调用Outer实例的new来创建Inner实例:
Outer.Inner inner = outer.new Inner();
这是因为Inner Class除了有一个this指向它自己,还隐含地持有一个Outer Class实例,可以用Outer.this访问这个实例。所以,实例化一个Inner Class不能脱离Outer实例。
Inner Class和普通Class相比,除了能引用Outer实例外,还有一个额外的“特权”,就是可以修改Outer Class的private字段,因为Inner Class的作用域在Outer Class内部,所以能访问Outer Class的private字段和方法。
观察Java编译器编译后的.class文件可以发现,Outer类被编译为Outer.class,而Inner类被编译为Outer$Inner.class。
Anonymous Class
还有一种定义Inner Class的方法,它不需要在Outer Class中明确地定义这个Class,而是在方法内部,通过匿名类(Anonymous Class)来定义。示例代码如下:
// Anonymous Class
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
outer.asyncHello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() { //匿名类
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
}; // 匿名类对象,作为语句的一部分,必须用分号结束
new Thread(r).start();
}
}
Main.main(null);
Hello, Nested
观察asyncHello()方法,我们在方法内部实例化了一个Runnable。Runnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:
Runnable r = new Runnable() {
// 实现必要的抽象方法...
};
匿名类和Inner Class一样,可以访问Outer Class的private字段和方法。之所以我们要定义匿名类,是因为在这里我们通常不关心类名,比直接定义Inner Class可以少写很多代码。
观察Java编译器编译后的.class文件可以发现,Outer类被编译为Outer.class,而匿名类被编译为Outer$1.class。如果有多个匿名类,Java编译器会将每个匿名类依次命名为Outer$1、Outer$2、Outer$3……
除了接口外,匿名类也完全可以继承自普通类。观察以下代码:
// Anonymous Class
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>() {}; // 匿名类!
HashMap<String, String> map3 = new HashMap<>() {
{
put("A", "1");
put("B", "2");
}
};
System.out.println(map3.get("A"));
}
}
Main.main(null);
1
map1是一个普通的HashMap实例,但map2是一个匿名类实例,只是该匿名类继承自HashMap。map3也是一个继承自HashMap的匿名类实例,并且添加了static代码块来初始化数据。观察编译输出可发现Main$1.class和Main$2.class两个匿名类文件。
Static Nested Class
最后一种内部类和Inner Class类似,但是使用static修饰,称为静态内部类(Static Nested Class):
// Static Nested Class
public class Main {
public static void main(String[] args) {
Outer.StaticNested sn = new Outer.StaticNested();
sn.hello();
}
}
class Outer {
private static String NAME = "OUTER";
private String name;
Outer(String name) {
this.name = name;
}
static class StaticNested {
void hello() {
System.out.println("Hello, " + Outer.NAME);
}
}
}
Main.main(null);
Hello, OUTER
用static修饰的内部类和Inner Class有很大的不同,它不再依附于Outer的实例,而是一个完全独立的类,因此无法引用Outer.this,但它可以访问Outer的private静态字段和静态方法。如果把StaticNested移到Outer之外,就失去了访问private的权限。
小结
Java的内部类可分为Inner Class、Anonymous Class和Static Nested Class三种;
Inner Class和Anonymous Class本质上是相同的,都必须依附于Outer Class的实例,即隐含地持有Outer.this实例,并拥有Outer Class的private访问权限;
Static Nested Class是独立类,但拥有Outer Class的private访问权限。
7.2.3 包装类
原始数据类型与包装类
因为Java是一个完全的面向对象的语言,几乎所有的方法都可以直接通过对象.方法()调用,然而8种基本数据类型的存在就很鸡肋,不能直接用int. char. double.来调用想用的功能,也没有继承封装等面向对象的思想,因此引入了包装类(封装类):
基本数据类型 | 包装类型 |
---|---|
byte | java.lang.Byte(父类Number) |
short | java.lang.Short(父类Number) |
int | java.lang.Integer(父类Number) |
long | java.lang.Long(父类Number) |
float | java.lang.Float(父类Number) |
double | java.lang.Double(父类Number) |
boolean | java.lang.Boolean(父类Object) |
char | java.lang.Character(父类Object) |
除了int和char,其他的都是直接首字母大写,前6个数值型的包装类都有一个共同的父类Number,Number又继承于Object
另外,还有两个包装类BigInteger和BigDecimal,没有相对应的基本类型,主要用于处理大数和浮点数高精度运算。BigInteger支持任意精度的整数,而BigDecimal支持任意精度的浮点数。
包装类在java.lang包,因此import java.lang.*可以导入所有的包装类。包装类属于引用类型。,具有方法和属性。
包装类既可以用new实例化,也可以使用静态方法valueOf将基本类型转换为包装类型:
包装类主要提供以下功能:
- 封装基本数据类型,使其具有类对象的特性,如封装类对象可以作为参数传递,可以作为返回值,可以作为成员变量。
- 为基本数据类型提供各种转换功能,如将字符串转换为基本数据类型,将基本数据类型转换为字符串。
有些数据结构(如 ArrayList 、 HashMap)不能存放原值类型,这时候需要使用包装类。
基本数据炻和其包装类之间的转换(以Interger为例):
1.基本数据类型转换为包装类:
Integer i = Integer.valueOf(10);
Integer k = new Integer(10);
Integer j = 10;
i
10
2.包装类转换为基本数据类型:
int x = i.intValue();
int y = j;
j
10
基本数据类型与包装类的区别:
- 在Java中,一切皆对象,但八大基本类型不是对象。
- 声明方式不同,基本类型直接声明,包装类需要先new一个对象。
- 存储方式及位置不同,基本类型直接存储在栈中,包装类存储在堆中。
- 初始值不同,基本类型有相应的默认值,如int为0,boolean类型为false;包装类默认值为null。
- 使用方式不同,比如与集合类合作使用时只能使用包装类。
装箱与拆箱
- 装箱是指将一个基本类型转换为一个对应封装的对象,如将Int型封装为Integer对象。
- 拆箱是指将一个封装对象转换为对应的基本类型,如将Integer对象拆箱为Int型。
请看下面的例子:
//手动装箱
int x=100; //定义一个基本类型变量x
Integer in=new Integer(x); //将基本数据类型x封装为Integer对象
System.out.println("封装类Integer类型in="+in); //输出Integer对象in
//手动拆箱
Float f=new Float(3.14F); ///将基本数据类型3.14F封装为Float对象
float y=f.floatValue(); //将Float对象f转换为基本数据类型float
System.out.println("基本类型float y"+y);
封装类Integer类型in=100
基本类型float y3.14
//自动装箱
int x=100; //定义一个基本类型变量x
Integer in=x; //将基本数据类型x封装为Integer对象
System.out.println("封装类Integer类型in="+in); //输出Integer对象in
//自动拆箱
Float f=new Float(3.14F); ///将基本数据类型3.14F封装为Float对象
float y=f; //将Float对象f转换为基本数据类型float
System.out.println("基本类型float y"+y);
使用自动装箱和拆箱时,需要注意自动装箱和拆箱可能引发空指针异常(NullPointerException)。
建议在以后程序开发时,使用手动拆箱和装箱。
常用方法
包装类提供了很多方法,这里介绍一些常用的方法:
- toString():将基本数据类型转换为字符串。
- valueOf():将字符串转换为基本数据类型。
- parseXXX():将字符串转换为基本数据类型。
- xxxValue():将包装类转换为基本数据类型。
- compareTo():比较两个包装类的大小。
- equals():比较两个包装类是否相等。
- hashCode():返回对象的哈希码值。
- clone():创建并返回此对象的副本。
- getClass():返回此对象的运行时类。
7.2.4 Java常用类库
JDK为开发人员提供了丰富的基础类库作为开发Java软件的基础,可以提高开发效率,降低开发难度。
日期类 Date
java.util.Date类表示特定的瞬间,精确到毫秒,它由GMT(Greenwich Mean Time)时间表示,也称为UTC(Coordinated Universal Time)时间。
java.time.LocalDate.now()
日期格式化类 SimpleDateFormat
java.text.SimpleDateFormat
随机数类 Random
java.util.Random