Java 对象与类

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 局部内部类

  • 不能有访问修饰符
  • 可以访问外部类的字段,和方法中的finaleffectively 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)。


如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值