跳至主要內容

Java

LincZero大约 14 分钟

Java

目录

类中函数

补充:

Java的函数都在类中

这是纯面向对象语言的通用标准,C#也是如此: 不允许在类外定义变量、方法、事件等,强调一切皆是对象的思想。 即便是主函数的main,也必须定义在某个类里面。

成员/方法访问控制

实例域的访问权限

  • 公有 实例域:[省略]
  • 私有 实例域:[省略]
  • final 实例域
  • static 静态域(静态实例域)
  • static final 静态常量

private 实例域(原则建议使用)

原则

  • 强烈建议将实例域标记为privat。public标记实例域,但这是一种极为不提倡的做法。
  • public数据域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装

原因

  • 如果某成员是一个只读域
    • 一旦在构造器中设置完毕,就没有任何一个办法可以对它进行修改,这样来确保该域不会受到外界的破坏。
  • 如果某成员不是只读域
    • 如果它只能通过给定的方法修改,则一旦这个域值出现了错误,只要调试这个方法就可以了。
    • 如果域是public的,则破坏这个域值的捣乱者有可能会出没在任何地方,不易调试。

注意

  • 注意不要编写返回引用可变对象的访问器方法。这近乎等同于将该可变对象的域标记为public
  • 如果需要返回一个可变数据域的拷贝,就应该使用clone

public 实例域

使用回public的场景

  • 为final实例域,本来就不可变

final 实例域

  • 可以将实例域定义为final。构建对象时必须初始化这样的域。 也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改
  • (final关键字还用于表示常量的定义)
  • final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域 (如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。 对于可变的类,使用final修饰符可能会对读者造成混乱。

static 静态域

如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于其他所有的实例域却都有自己的一份拷贝。

应用举例:类成员计数 / 实例id

"static"关键字的路径依赖问题:

在绝大多数的面向对象程序设计语言中,静态域被称为类域。术语“static”只是沿用了C++的叫法,名字上并无实际意义。

  • 起初,C引入关键字static是为了表示退出一个块后依然存在的局部变量。此时术语“static”是有意义的:变量一直存在,当再次进入该块时仍然存在
  • 随后,static在C中有了第二种含义,表示不能被其他文件访问的全局变量和函数。为了避免引入一个新的关键字,关键字static被重用了
  • 最后,C++第三次重用了这个关键字,与前面赋予的含义完全不一样,这里将其解释为:属于类且不属于类对象的变量和函数。这个含义与Java相同

static final 静态常量

静态变量使用得比较少,但静态常量却使用得比较多

public class Math
{
    public static final double PI = 3.14159265358979323846;
}

前面曾经提到过,由于每个类对象都可以对公有域进行修改,所以,最好不要将域设计为public。然而,公有常量(即final域)却没问题。

绕过static final(不要这样写)

System.out

public class System
{
    public static final PrintStream out = ...;
}

System.out = new PrintStream(...);	// Error--out is final

按理final不可被赋值。但发现有一个setOut方法,它可以将System.out设置为不同的流,修改了final变量的值

原因在于,setOut方法是一个本地方法,而不是用Java语言实现的。本地方法可以绕过Java语言的存取控制机制。这是一种特殊的方法,在自己编写程序时,不应该这样处理。

类方法的访问权限

  • 公有方法:略
  • 私有方法:由于公有数据非常危险,所以应该将所有的数据域都设置为私有的
    • 场景:
    • 将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密
    • 或者需要一个特别的协议以及一个特别的调用次序

4种访问权限

关键字/访问修饰符(access modifier)访问权限-----------------------------------------------------------------------
public公有
private私有
static静态

static 静态方法

静态方法是一种不能向对象实施操作的方法。可以认为静态方法是没有this参数的方法

但是,静态方法可以访问自身类中的静态域。

例如,Math类的pow方法就是一个静态方法。在运算时,不使用任何Math对象换句话说,没有隐式的参数

Math.pow(x,a);

使用场景

  • 方法不需要访问对象状态,因为它需要的所有参数都通过显式参数提供(例如Math.pow)
  • 方法只需要访问类的静态字段(例如Employee.getNextId)

与C++不同

好像C++中只有static方法才能操作static成员,但Java中似乎没有此限制

Java中的静态域与静态方法在功能上与C++相同。但是,语法书写上却稍有所不同。

  • Java:无需构造对象。使用.操作符直接访问,如Math.pow
  • C++:无需构造对象。使用::操作符访问自身作用域之外的静态域和静态方法,如Math::PI
工厂方法(静态方法)

工厂方法是静态方法

  • 作用

    • 可以在不构建对象和不使用构造器的前提下构造对象(构造器也是静态方法吧)
  • 举例

    类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。

    NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
    NumberFormat percentFormatter = NumberFormat.getPercentInstance();
    double x = 0.1;
    System.out.println(currencyFormatter.format(x));	// prints $0.10
    Syste,.out.println(percentFormatter.format(x));		// prints 10%
    

为什么NumberFormat类不利用构造器完成这些操作呢?这主要有两个原因:

  • 无法命名构造器。构造器的名字必须与类名相同。但是这里希望有两个不同的名字,分别得到货币实例和百分比实例
  • 使用构造器时,无法改变所构造对象的类型。而工厂方法实际将返回DecimalFormat类的对象,这是NumberFormat的一个子类
main方法(静态方法)

main方法也是静态方法

事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需要的对象。

  • 作用

    • 每一个类可以有一个main方法。这是一个常用于对类进行单元测试的技巧
    • 作用有点类似于Python中的if __name__=='main':
    • 或者也可以选择在同一文件下创建一个用于单元测试的类
  • 举例

    class Employee
    {
        public Employee(){}
        public static void main(String[] args)
        {
            Employee e = new Employee();
            Syste,.out.println(e.method(10));
        }
    }
    
    // shell:
    java Employee		// 如果想要独立地测试Employee类,只要单独执行
    java Application	// 如果该类类是一个更大型应用程序的一部分,则不直接运行时其main方法永远不会执行
    

构造和析构函数

构造函数

构造器(constructor)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。

Java构造器的工作方式与C++一样。但是,要记住所有的Java对象都是在堆中构造的,构造器总是伴随着new操作符一起使用。

构造器与其他的方法有一个重要的不同。构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的

james = new Employee("James Bond", 100000, 1950, 1, 1);
jame.Employee("James Bond", 100000, 1950, 1, 1);		// ERROR

不要在构造器中定义与实例域重名的局部变量

class Employee
{
    // instance field
    private String name;
    private double salary;
    private Localdate hireDay;
    
    // constructor
    public Employee(String n, double s, int year, int month, int day)
    {
        String name = n;	// Error
        double salary = s;	// Error
    }
}

方法一:默认域初始化

如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null

然而,只有缺少程序设计经验的人才会这样做。如果不明确地对域进行初始化,就会影响程序代码的可读性

与C++相似地:

仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器

方法二:显式域初始化

与C++相似地:

  • Java:可以在执行构造器之前,先执行赋值操作

    class Employee
    {
        private String name = "";
        // ...
    }
    
  • C++:

    C++11也可以进行类内初始化(C++前不能),也有功能相似的成员初始化列表(Java没有)

    class Employee
    {
        Employee::Employee(String n, double s, int y, int m, int d)
            :name(n), salary(s), hireDay(y,m,d)
        {
            // ...    
        }
    }
    
    

方法三:初始化块(initialization block)

这种机制不是必需的,也不常见

无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化

首先运行初始化块,然后才运行构造器的主体部分。

class Employee
{
    private static int nextId;
    private int id;
    private String name;
    private double salary;
    
    // 初始化块 object initialization block
    {
        id = nextId;
        nextId++;
	}
    
    public Employee(String n, double s)
    {
        name = n;
        salary = s;
    }
    
    public Employee()
    {
        name = "";
        salary = 0;
    }
    // ...
}

构造器调用构造器

与C/C++不同:构造器调用构造器

  • C++:一个构造器不能调用另一个构造器。在C++中,必须将抽取出的公共初始化代码编写成一个独立的方法
  • Java:可以,调用构造器的具体处理步骤如下
    1. 所有数据域被初始化为默认值(0、false或null)
    2. 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块
    3. 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体
    4. 执行这个构造器的主体

参数名命名习惯

与C/C++不同:成员函数参数命名习惯

  • C++

    • 经常用下划线或某个固定的字母(一般选用m或x)作为实例域的前缀
    • 例如,_salary、mSalary或xSalary
  • Java

    • 程序员则喜欢在每个参数前面加上一个前缀“a”

      public Employee(String aName, double aSalary)
      {
          name = aName;
          salary = aSalary;
      }
      

析构函数、finalize方法

与C/C++不同:析构函数

  • C++:有显式的析构器方法:~+类名

  • Java:有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器

    当然,某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要。

    可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用

  • 补充

    在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用

    如果某个资源需要在使用完毕后立刻被关闭,那么就需要由人工来管理。对象用完时,可以应用一个close方法来完成相应的清理操作

其他函数

更改器方法与访问器方法(C++中的const成员函数)

与C/C++不同:访问器方法和静态方法

  • Java:只访问对象而不修改对象的方法有时称为访问器方法(accessor method) 例如:LocalDate.getYear和GregorianCalendar.get就是访问器方法。 在Java语言中,访问器方法与更改器方法在语法上没有明显的区别
  • C++:带有const后缀的方法是访问器方法;默认为更改器方法。 但C++中似乎没有这种叫法,只是简单称为const成员函数

内联方法(虚拟机自动设置)

与C/C++不同

  • C++
    • 通常在类的外面定义方法
    • 如果在类的内部定义方法,这个方法将自动地成为内联(inline)方法。
  • Java
    • 所有的方法都必须在类的内部定义
    • 但并不表示它们是内联方法。是否将某个方法设置为内联方法是Java虚拟机的任务。 即时编译器会监视调用那些简洁、经常被调用、没有被重载以及可优化的方法。

普通方法(Java)

(该节指与类关系不大的方法,毕竟其他语言中的函数并不强制要求要在类中声明)

方法参数

“按……调用”(call by)是一个标准的计算机科学术语,它用来描述各种程序设计语言中方法参数的传递方式

  • 按值调用(call by value)表示方法接收的是调用者提供的值
  • 按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。

与C/C++不同

  • Java:程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝。方法不能修改传递给它的任何参数变量的内容

    但注意的是:Java的对象实例本来就是对象的引用,按值引用后依然是对象的引用。可以通过对象方法修改对象实例所引用的对象

  • C++:可以自由选择按值调用(call by value)或按引用调用(call by reference)

    例如:void tripleValue(double&x),一个方法是按值调用还是按引用调用需要看函数原型才能知道

  • 不可单纯将Java的对象实例理解为C++的引用,而必须要看成指针

    • 比如:不能编写一个交换两个雇员对象的方法,因为交换的只是拷贝进方法中的两个对象实例的地址,而并不能改变外部的两个对象实例的地址

方法重载

  • 重载(overloading)

    • 如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便产生了重载。
  • 重载解析(overloading resolution)

    • 编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。
    • 如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好。
  • 方法的签名(signature)

    • 要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名

    • 返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法

    • 例如:String类有4个称为indexOf的公有方法,它们的签名是

      indexOf(int);
      indexOf(int, int);
      indexOf(String);
      indexOf(String, int);
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      

其他

隐式(implicit)参数与显式(explicit)参数

在每一个方法中,关键字this表示隐式参数,是类实例的指针

在Java中不用this也能调用,但有些程序员更偏爱加this,因为这样可以将实例域与局部变量明显地区分开来