1、对象与对象变量
对象变量
和对象
的区别是什么?我们看看下面的代码。
Date deadline = new Date();
这段代码中,我们构造了一个 Date 类型的对象,并将这个对象的引用(地址)赋值给了 deadline,这个 deadline 即是对象变量。(Java 中的对象变量可以看作 C++ 中的对象指针)
2、访问修饰符
常规类
(非内部类)的访问修饰符:
- public:被修饰的类可以被任意类访问。在一个源文件中,只能有一个public修饰的类,并且源文件名必须与public类的名字相同。
- 缺省:默认的访问修饰符,被修饰的类只能被同一个包中的类访问。
字段和方法
的访问修饰符:
- public:被修饰的字段和方法可以被任意类访问。
- protected:被修饰的字段和方法可以被所有
子类
和同一个包中的类访问。需要注意的是,在不同包中,子类可以访问的是继承
的受保护字段及方法,而不能访问父类
对象的受保护字段及方法(避免滥用保护机制,不能通过派生子类来访问超类对象受保护的字段) - 缺省:默认的访问修饰符,被修饰的字段和方法只能被同一个包中的类访问。
- private:被修饰的字段和方法只能被所属的类访问。
可能protected
有点难理解,下面用代码看看。
package package1;
public class Employee {
protected double salary;
protected void setSalary(double s) {
salary = s;
}
}
package package2;
import package1.Employee;
public class Manager extends Employee {
public void test1() {
// 在不同包中,子类可以访问继承的 protected 字段和方法
System.out.println("salary=" + salary);
setSalary(1800.0);
}
public void test2(Employee e) {
// 在不同包中,子类不能访问父类对象的 protected 字段和方法
System.out.println("salary=" + e.salary);
e.setSalary(1800.0);
}
}
如上所示,Manager 继承 Employee 类,这两个类在不同包中。在 Manager 类中 test1 方法可以正确执行,而 test2 方法会报错。
3、类
3.1 字段
实例字段的初始化顺序
:
- 如果构造器的第一行调用了另一个构造器,则执行第二个构造器。
- 执行默认初始化
- 按照在类声明中出现的顺序,执行显式初始化、初始化块
- 执行构造器主体代码
静态字段
在类第一次加载的时候,按照类声明中出现的顺序执行显式初始化、静态初始化块。
示例代码:
class Employee {
static { staticFoo(); } // 2.第一个静态初始化块
private static int nextId = 7;
static { System.out.println("nextId=" + nextId); } // 3.第二个静态初始化块
{ foo(); } // 6.第一个初始化块
public static void staticFoo() {
System.out.println("nextId=" + nextId);
}
private int id;
private String name = "L";
private double salary;
{
// 7.第二个初始化块
System.out.println("id=" + id + ",name=" + name + ",salary=" + salary);
}
public Employee() {}
public Employee(String n, double s) {
// 8.执行构造器主体代码
name = n;
salary = s;
}
public Employee(String n) {
this(n, 1800.0); // 5.调用另一个构造函数
System.out.println("id=" + id + ",name=" + name + ",salary=" + salary);
}
public void foo() {
System.out.println("id=" + id + ",name=" + name + ",salary=" + salary);
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.company.Employee"); // 1.类加载
System.out.println("----------");
Employee e = new Employee("张三"); // 4.创建实例对象
}
}
运行结果:
nextId=0
nextId=7
----------
id=0,name=null,salary=0.0
id=0,name=L,salary=0.0
id=0,name=张三,salary=1800.0
通过上述测试,表明初始化顺序确实如此。(注意看代码注释标出的顺序,可以打断点调试看看)
3.2 方法
构造器
:
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。
更改器方法
和访问器方法
:
更改器方法会改变对象的状态,而访问器方法只访问对象而不修改对象。(注意不要编写返回可变对象引用的访问器方法,这会破坏类的封装性
!)
class Employee {
private Date hireDay;
...
public Date getHireDay() {
return hireDay; // BAD
}
}
如上所示,如果 Employee 类型的对象调用 getHireDay 方法,将会返回私有
的对象引用,这时我们就可以通过这个引用修改对象
的值!(建议使用克隆返回一个副本)
重载
方法:方法的名字相同,参数类型不同。
要完整地描述一个方法,需要指定方法名以及参数类型,这叫做方法的签名
。返回类型不是方法签名的一部分,即不能有两个名字相同、参数类型也相同却有不同返回类型的方法。
在进行方法调用时,如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,编译器就会报告一个错误。
参数数量可变
的方法:使用...
可以表示参数数量可变的方法,如下所示。
public class Main {
public static double max(double... values) {
double largest = Double.NEGATIVE_INFINITY;
for(double v : values) {
if(v > largest) largest = v;
}
return largest;
}
public static void main(String[] args) {
double m = max(3.1, 40.4, -5); // 编译器将 new double[]{3.1, 40.4, -5} 传递给 max 方法
System.out.println(m);
}
}
其他知识点:
一个方法可以访问所属类的所有对象的私有数据。
每一个类都可以有一个 main 方法,常用于对类进行单元测试。
Java 中所有方法都必须在类的内部定义,但并不表示它们是内联方法。
Java 程序设计语言总是按值调用
,即方法得到的是所有参数值的一个副本。
3.3初始化块
可以将初始化块放在字段定义之前,这时只能在块中对字段进行赋值,而不能读取字段的值。
class Employee {
{
System.out.println(id); // 非法
id = 7; // 合法
}
private int id;
private String name = "L";
private double salary;
public Employee() {}
}
4、包
一般使用因特网域名以逆序的形式作为包名,然后对不同工程使用不同的子包,以防止相同名字的类产生冲突。
一个类可以使用所属包的所有类,以及其他包中的公共类。我们可以使用完全限定名
来访问其他包中的公共类:
java.time.LocalDate today = java.time.LocalDate.now();
也可以使用import
语句,导入一个特定类或者整个包。
import java.time.LocalDate;
import java.time.*;
导入后就可以直接使用类名,而不用写出类所属的包:
LocalDate today = LocalDate.now();
需要注意的是,import 语句应该位于package
语句的后面,并且只能使用星号(*)导入一个包,而不能一次导入多个包,如import java.*
或import java.*.*
。
嵌套的包
之间没有任何关系,每一个包都是独立的类集合。用星号导入一个包中的所有类后,使用其子包仍需要显示导入。(如 java.util 包和 java.util.jar 包)
当导入的包存在命名冲突
时,例如 java.util 和 java.sql 包都有 Date 类,此时编译器无法确定使用哪一个 Date 类,会出现编译错误。这时需要在类名之前加上完整的包名来解决此问题。
编译器将 java 文件编译为 .class 文件后,.class 文件中的字节码使用的都是完全限定名
。我不信,怎么办?尝试分析一下字节码文件吧,我们对如下代码进行编译后,对生成的字节码文件进行反编译试试看。
package com.company;
import java.util.*;
public class Main {
public static void main(String[] args) {
Date date = new Date();
}
}
使用javap
命令反编译看看。。
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #7 // class java/util/Date
3: dup
4: invokespecial #9 // Method java/util/Date."<init>":()V
7: astore_1
8: return
}
虽然看不太懂,但看到的确实都是完全限定名诶。。(以后再看看)
之前说的 import 导入都是导入类,而还有一种导入可以导入静态方法和静态字段,我们称之为静态导入
,如下所示:
import static java.lang.System.*;
import static java.lang.Math.*;
public class Main {
public static void main(String[] args) {
out.println(PI);
out.println(sqrt(2));
}
}
这段代码使用import static
导入了 System 类和 Math 类的静态方法和静态字段。
5、内部类
使用内部类的主要原因:
- 内部类可以对同一个包中的其它类隐藏
- 内部类的方法可以访问外围类的所有数据,包括私有数据
5.1 成员内部类
- 可以被所有的访问修饰符修饰
public class Main {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
}
}
class OuterClass {
private int outerField;
public void OuterMethod() {}
public class InnerClass {
private int InnerField;
public void InnerMethod() {
System.out.println(outerField);
System.out.println(OuterClass.this.outerField);
}
}
}
5.2 局部内部类
- 不能有访问修饰符
- 可以访问外部类的字段,和方法中的
final
或effectively final
的局部变量
public class Main {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass(7);
Demo demo = outerClass.OuterMethod();
demo.InnerMethod();
}
}
interface Demo {
void InnerMethod();
}
class OuterClass {
private int outerField;
public OuterClass() {}
public OuterClass(int outerField) {
this.outerField = outerField;
}
public Demo OuterMethod() {
String s = "Hello World!";
class InnerClass implements Demo {
private int InnerField;
@Override
public void InnerMethod() {
System.out.println(s);
System.out.println(outerField);
}
}
return new InnerClass();
}
}
需要注意,编译器会检测内部类对局部变量的访问,为每一个变量建立相应的实例字段,我们反编译内部类看看结果:
class com.company.OuterClass$1InnerClass implements com.company.Demo {
private int InnerField;
final java.lang.String val$s;
final com.company.OuterClass this$0;
com.company.OuterClass$1InnerClass();
public void InnerMethod();
}
确实多出一个 String 类型的字段,还有一个this$0
字段指示外围类的引用。
5.3 匿名内部类
public Demo OuterMethod() {
String s = "Hello World!";
return new Demo() {
@Override
public void InnerMethod() {
System.out.println(s);
System.out.println(outerField);
}
};
}
5.4 静态内部类
为什么内部类中声明的所有静态字段都必须是 final 的?(书上说的)
为什么常规内部类不能有静态字段和方法,只有静态内部类可以有静态字段和方法?
在 Java 8 中,常规内部类如果有静态字段和方法将会报错,如图所示:
网上很多解释都是说违背了static
语义,但是感觉解释得不是很让人信服。
而且,我在高版本的 JDK 中测试,已经不会报错了。所以,这个规定到底要不要遵守呢?(保留意见)
public class Main {
public static void main(String[] args) {
OuterClass.InnerClass.k = 100;
System.out.println(OuterClass.InnerClass.k);
}
}
class OuterClass {
public class InnerClass {
public static int k = 7;
}
}
以上代码不会报错(高版本 JDK)。
如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--