最全面的Java面向对象讲解(五)_Java代码块、static和final关键字

一、JavaBean规范

什么是JavaBean

        JavaBeans是Java中一种特殊的类,可以将多个对象封装到一个对象(bean)中。特点是可序列化,提供无参构造器,提供getter方法和setter方法访问对象的属性。名称中的“Bean”是用于Java的可重用软件组件的惯用叫法。
                —以上源自维基百科

        JavaBean是一种java语言写成的可重用组件(类)。

        必须遵循一定的规范:
                1、类必须使用public修饰。
                2、必须保证有公共无参数构造器(推荐显式的定义)。
                3、属性封装,提供了属性的操作手段(给属性赋值,获取属性值)。

        在Java中,有很多`class`的定义都符合这样的规范:
                - 若干`private`实例字段;
                - 通过`public`方法来读写实例字段。

示例代码:

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

如果读写方法符合以下这种命名规范:

// 读方法:
public Type getXyz()
    
// 写方法:
public void setXyz(Type value)

        那么这种class被称为JavaBean:

        上面的字段是xyz,那么读写方法名分别以get和set开头,并且后接大写字母开头的字段名Xyz,因此两个读写方法名分别是getXyz()和setXyz()。

        boolean字段比较特殊,它的读方法一般命名为isXyz():

// 读方法:
public boolean isChild()
    
// 写方法:
public void setChild(boolean value)

        我们通常把一组对应的读方法(getter)和写方法(setter)称为属性(property)。
                例如,`name`属性:
                对应的读方法是`String getName()`
                对应的写方法是`setName(String)`

        只有`getter`的属性称为只读属性(read-only),例如,定义一个age只读属性:
                对应的读方法是`int getAge()`
                无对应的写方法`setAge(int)`

        类似的,只有`setter`的属性称为只写属性(write-only)。

        很明显,只读属性很常见,只写属性不常见。

        属性只需要定义`getter`和`setter`方法,不一定需要对应的字段。

例如,child只读属性定义如下:

public class Person {
    private String name;
    private int age;

    public String getName() { 
        return this.name; 
    }
    
    public void setName(String name) { 
        this.name = name; 
    }

    public int getAge() { 
        return this.age; 
    }
    
    public void setAge(int age) { 
        this.age = age; 
    }

    public boolean isChild() {
        return age <= 6;
    }
}

        可以看出,gettersetter也是一种数据封装的方法。

JavaBean的作用

        JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。
    
        用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

        此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。

        通过IDE,可以快速生成getter和setter。

public class UserBean {
    private int id;
    private String userNmae;
    private String userPwd;
    private String address;
    private Date birthday;
    private String phone;
    private char sex;

    public UserBean(){}

    public UserBean(int id, String userNmae, String userPwd, String address, Date birthday, String phone, char sex) {
        this.id = id;
        this.userNmae = userNmae;
        this.userPwd = userPwd;
        this.address = address;
        this.birthday = birthday;
        this.phone = phone;
        this.sex = sex;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUserNmae() {
        return userNmae;
    }

    public void setUserNmae(String userNmae) {
        this.userNmae = userNmae;
    }

    public String getUserPwd() {
        return userPwd;
    }

    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }
}

public class JavaBeanTest {

    public static void main(String[] args) {
        JavaBeanTest javaBeanTest = new JavaBeanTest();
        javaBeanTest.addUser("zhangsan","123","北京",new Date(),"110",'男');


        UserBean userBean = new UserBean(1001,"lisi","123","北京",new Date(),"110",'男');
        javaBeanTest.addUser(userBean);
    }

    public void addUser(String userName, String userPwd, String address, Date birthday,String phone,char sex){
        // ...调用其他方法
        // 其他方法的定义
        // otherMethod(String userName, String userPwd, String address, Date birthday,String phone,char sex)
        otherMethod(userName,userPwd,address,birthday,phone,sex);
    }

    public void addUser(UserBean userBean){
        // ...调用其他方法
        // 其他方法的定义
        // otherMethod(UserBean userBean);
        otherMethod(userBean);
    }

    public void otherMethod(String userName, String userPwd, String address, Date birthday,String phone,char sex){

    }

    public void otherMethod(UserBean userBean){

    }

}

小结

        JavaBean是一种符合命名规范的class,它通过getter和setter来定义属性;

        属性是一种通用的叫法,并非Java语法规定;

        可以利用IDE快速生成getter`和`setter;

二、关键字:static

理解static

        当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份,例如所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

public class Chinese {
    String name;
    int age;
    // 体会country设定为成员变量
    String country;

    public Chinese(){}

    public Chinese(String name, int age, String country) {
        this.name = name;
        this.age = age;
        this.country = country;
    }
}

        创建两个Chinese对象:
                Chinese c1 = new Chinese("张三",12,"CHN");
                Chinese c2 = new Chinese("李四",14,"CHN");
    
        Chinese类中的变量name、age、country是一个实例变量(instance variable),它属于类的每一个对象,不能被同一个类的不同对象所共享。

        正常来说:所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

        如果想让一个类的所有实例共享数据,就用类变量!

类属性、类方法的设计思想

        类属性作为该类各个对象之间共享的变量。在设计类时,分析哪些属性不因对象的不同而改变,将这些属性设置为类属性。相应的方法设置为类方法。

        如果方法与调用者无关,则这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

static使用范围和特点

        static使用范围:
                在Java类中,可用static修饰属性、方法、代码块、内部类。

        被修饰后的成员具备以下特点:
                随着类的加载而加载。
                优先于对象存在。
                修饰的成员,被所有对象所共享。
                访问权限允许时,可不创建对象,直接被类调用。

        在《Java编程思想》P86页有这样一段话:
                “static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。

        很显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

static主要作用

        第一,为特定数据类型或对象分配单一的存贮空间,而与创建对象的个数无关。
        第二,希望某个方法或属性与类而不是对象关联在一起。也就是说: 在不创建对象的情况下就可以通过类来直接调用方法或使用类的属性。

类变量(class Variable)

        虽然java语言中没有全局的概念,但可以通过static关键字来达到全局的效果。

        java类提供了三种类型的变量:用static关键字修饰的静态变量、没有static关键字修饰的实例变量以及在方法里面定义的局部变量。

        静态变量属于类,在内存中只有一个副本(所有实例都指向同一个内存地址)。只要静态变量所在的类被加载,这个静态类就会被分配空间,因此就可以被使用。对静态变量的引用有两种方式,分别为“类.静态变量”和“对象.静态变量”。

        实例变量属于对象,只有对象被创建后,实例变量才会被分配空间,才能被使用,他在内存中存在多个副本,只能用“对象.实例变量”的方式来引用。

        静态变量只有一个,被类所拥有,所有的对象都共享这个静态变量,而实例对象与具体对象有关。

        静态属性举例:System.out; Math.PI;

public class Chinese {
    String name;
    int age;
    // 体会country设定为类成员(静态变量)
    static String country;

    public Chinese() {
    }

    // 类变量随着类的加载而加载,优先于对象存在,那么就没必要在构造器里面体现。
    public Chinese(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class ChineseTest {
    public static void main(String[] args) {
        // 初始化类变量,通过类名调用
        Chinese.country = "中国";

        // 创建两个Chinese对象
        Chinese c1 = new Chinese();
        c1.name = "张三";
        c1.age = 12;

        Chinese c2 = new Chinese();
        c2.name = "李四";
        c2.age = 14;

        // 通过对象来调用类变量
        System.out.println(c1.country); // 中国
        System.out.println(c2.country); // 中国

        Chinese.country = "CHN";

        // 类变量存储在方法区的静态域,在内存中仅存在一份,一旦发生修改,全部改变;
        System.out.println(c1.country); // CHN
        System.out.println(c2.country); // CHN

    }
}

类变量 vs 实例变量内存解析:

 局部变量、成员变量、静态变量的区别:

成员变量局部变量静态变量
定义位置在类中 , 方法外方法中 , 或者方法的形式参数在类中 , 方法外, 由static修饰的成员变量
初始化值有默认初始化值无 , 先定义 , 赋值后才能使用有默认初始化值
调用方式对象调用通过变量名对象调用,类名调用
存储位置堆中栈中方法区
生命周期与对象共存亡与方法共存亡与类共存亡

类变量应用举例:

需求如下:
        创建Person类,属性有id、name、age等,定义封装这些属性的方法。定义属性total用来记录创建Person对象的个数。
        思考:total定义为成员变量还是类变量?

public class Person {
    private int id;
    private String name;
    private int age;
    public static int total = 0;

    public Person(){
        total++;
    }

    public Person(int id, String name, int age) {
        this();
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class PersonTest {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setId(1001);
        p1.setName("张三");
        p1.setAge(12);

        Person p2 = new Person(1002,"李四",14);

        System.out.println("total:" + Person.total);
    }
}

        上述代码,有什么弊端?
                total应该是每创建一个Person对象就加1,但是因为其访问权限是public,所以可以直接被访问到,然后进行赋值,显然不合适;

        怎么解决?
                很多同学都想到了私有化,然后提供对应的getter和setter方法,但是setter方法有必要存在吗?显然没有必要。
        public int getTotal(){
            return total;
        }

        但是新的问题又来了,如果定义为成员方法,那么必须通过对象才能调用,也就是创建对象之前,我们是没有办法获取到人的数量的;解决方法:静态方法。

如何确定一个属性是否要声明为static的?

        属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
        类中的常量也常常声明为static。

类方法(class method)

        与变量类似,java类同时也提供了static方法与非static方法。static方法是类方法(静态方法),不需要创建对象就可以被使用,而非static方法是对象的方法(成员方法),只有对象被创建出来后才可以被使用。

public class Person {
    private int id;
    private String name;
    private int age;
    private static int total = 0;

    public Person(){
        total++;
    }

    public Person(int id, String name, int age) {
        this();
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static int getTotal(){
        return total;
    }
}

public class PersonTest {
    public static void main(String[] args) {
        System.out.println("total:" + Person.getTotal());

        Person p1 = new Person();
        p1.setId(1001);
        p1.setName("张三");
        p1.setAge(12);

        Person p2 = new Person(1002,"李四",14);

        System.out.println("total:" + Person.getTotal());
    }
}

static方法的执行机制:

        static 方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和成员方法,因为当static方法被调用的时候,这个类的对象可能还没有被创建,即使已经被创建, 也无法确定调用哪个对象的方法。同理static方法也不能访问非static类型的变量。

  但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。

示例代码:

public class MyObject {
    // 类变量
    private static String staticField = "staticProperty";
    // 成员变量
    private String field = "property";

    public MyObject(){}

    // 成员方法
    public void method(){
        // 成员方法中调用类变量
        System.out.println(staticField);

        // 成员方法中调用成员变量
        System.out.println(field);

        // 成员方法中调用静态方法
        staticShow();

        // 成员方法中调用成员方法
        show();
    }
    
    // 静态方法
    public static void staticMethod(){
        // 静态方法中调用类变量
        System.out.println(staticField);

        // 静态方法中调用成员变量
        // 报错:Non-static field 'field' cannot be referenced from a static context
        System.out.println(field); 

        // 静态方法中调用静态方法
        staticShow();

        // 静态方法中调用成员方法
        // 报错:Non-static method 'show()' cannot be referenced from a static context
        show();
    }

    public void show(){
        System.out.println("我是成员方法");
    }

    public static void staticShow(){
        System.out.println("我是静态方法");
    }
}

        在上面的代码中,由于staticMethod方法是独立于对象存在的,可以直接用过类名调用。假如说可以在静态方法中访问非静态方法/变量的话,那么如果在main方法中有下面一条语句:
        MyObject.staticMethod();

        此时对象都没有,field根本就不存在,所以就会产生矛盾了。同样对于方法也是一样,由于你无法预知在show()方法中是否访问了非静态成员变量,所以也禁止在静态成员方法中访问非静态成员方法。

  而对于非静态成员方法,它访问静态成员方法/变量显然是毫无限制的。

  因此,如果说想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

        static的一个很重要的的用途的是实现单例模式。(设计模式相关知识,在后面讲解)

        因为不需要实例就可以访问static方法,因此static方法内部不能有this和super。  

        static修饰的方法不能被重写。

如何确定一个方法是否要声明为static的?

        操作静态属性的方法,通常设置为static的。
        工具类中的方法,习惯上声明为static的。 比如:Math、Arrays、Collections。

关于静态属性和静态方法的使用,大家都从生命周期的角度去理解。

static代码块

        static代码块在类中是独立与成员变量和成员函数的代码块,他不在任何一个方法体内,JVM在加载类的时候会执行static代码块,如果有多个static代码块,JVM将会按顺序来执行,static代码块经常会被用来初始化静态变量,需要注意的是static代码块只会被执行一次。

  static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。

  为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。

        下面看个例子:

class Person{
    private Date birthDate; // 出生日期

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    // 成员方法:这个方法的作用是判断是否是1986年1994年出生的
    boolean isBornBoomer() {
        Date startDate = new Date(1986);
        Date endDate = new Date(1994);
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

        isBornBoomer是用来这个人是否是1986-1994年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:

class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

        因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

static修饰内部类

        static内部类是指被声明为static的内部类,他可以不依赖于外部类实例对象而被实例化,而通常的内部类需要外部类实例化后才能实例化。

        静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态成员方法,只有内部类才能被申明为static。

        内部类相关知识,在后面讲解。

public class Outer{
    public static class Inner{
        
    }
}

static关键字的误区

问题一:static关键字会改变类中成员的访问权限吗?

        有些初学的朋友会将java中的static与C/C++中的static关键字的功能混淆了。

        在这里只需要记住一点:与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。

        看下面的例子就明白了:

        提示错误"total has private access in Person",这说明static关键字并不会改变变量和方法的访问权限。

问题二:能通过this访问静态成员变量吗?

        虽然对于静态方法来说没有this,那么在非静态方法中能够通过this访问静态成员变量吗?

        先看下面的一个例子,这段代码输出的结果是什么?

public class Main {
    static int value = 33;

    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }

    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

        这里面主要考察 对this和static的理解。this代表什么?this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。

        在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)v

问题三:static能作用于局部变量么?

        在C/C++中static是可以作用域局部变量的,但是在Java中切记:static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定。

理解main方法的语法

        由于Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public,又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。

        又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

命令行参数用法举例:

public class CommandPara {
    // main()方法可以作为我们与控制台交互的方式。
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println("args[" + i + "] = " + args[i]);
        }
    }
}

1、通过IDEA配置参数:

 2、通过cmd窗口执行:

# 运行程序CommandPara.java
java CommandPara 张三 李四 "王五"

三、代码块

        代码块(或初始化块)的作用:
                对Java类或对象进行初始化。

        代码块(或初始化块)的分类:
                一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block),没有使用static修饰的,为非静态代码块。

        static代码块通常用于初始化static的属性:
        class Person {
            public static int total;
            static {
                total = 100;//为total赋初值
            }
            //其它属性或方法声明
            …… 

        执行顺序:(优先级从高到低。)
                静态代码块 > mian方法 > 构造代码块 > 构造方法 > 普通代码块;

        其中静态代码块只执行一次。构造代码块在每次创建对象是都会执行。

普通代码块

// 普通代码块:在方法或语句中出现的{}就称为普通代码块。普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定--“先出现先执行”
public class GeneralCodeBlock01{
    public static void main(String[] args){

        {
            int x=3;
            System.out.println("1,普通代码块内的变量x="+x);    
        }

        int x=1;
        System.out.println("主方法内的变量x="+x);

        {
            int y=7;
            System.out.println("2,普通代码块内的变量y="+y);    
        }
    }
}

执行结果:

1,普通代码块内的变量x=3
主方法内的变量x=1
2,普通代码块内的变量y=7

构造代码块

// 构造块:直接在类中定义且没有加static关键字的代码块称为{}构造代码块。
// 构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类构造函数。

public class ConstructorCodeBlock02 {
    {
        System.out.println("第一构造块");
    }

    public ConstructorCodeBlock02() {
        System.out.println("构造方法");
    }

    {
        System.out.println("第二构造块");
    }

    public static void main(String[] args) {
        new ConstructorCodeBlock02();
    }
}

执行结果:

第一构造块
第二构造块
构造方法

静态代码块

// 静态代码块:在java中使用static关键字声明的代码块。静态块用于初始化类,为类的属性初始化。
// 每个静态代码块只会执行一次。由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。
// 如果类中包含多个静态代码块,那么将按照"先定义的代码先执行,后定义的代码后执行"。
// 注意:1 静态代码块不能存在于任何方法体内。2 静态代码块不能直接访问成员变量和成员方法,需要通过类的实例对象来访问。

class Code {
    {
        System.out.println("Code的构造块");
    }

    static {
        System.out.println("Code的静态代码块");
    }

    public Code() {
        System.out.println("Code的构造方法");
    }
}

public class StaticCodeBlock03 {
    {
        System.out.println("CodeBlock03的构造块");
    }

    static {
        System.out.println("CodeBlock03的静态代码块");
    }

    public StaticCodeBlock03() {
        System.out.println("CodeBlock03的构造方法");
    }

    public static void main(String[] args) {
        System.out.println("CodeBlock03的主方法");
        new Code();
        new StaticCodeBlock03();
    }
}

执行结果:

CodeBlock03的静态代码块
CodeBlock03的主方法
Code的静态代码块
Code的构造块
Code的构造方法
CodeBlock03的构造块
CodeBlock03的构造方法

四、关键字:final

        在java中,final的含义在不同的场景下有细微的差别,但总体上来说,它指的是“这是不可变的”。

        在Java中声明类、变量和方法时,可使用关键字final来修饰,表示“最终的”。
                final标记的类不能被继承。提高安全性,提高程序的可读性。
                        String类、System类、StringBuffer类
                final标记的方法不能被子类重写。
                        比如:Object类中的getClass()。
                final标记的变量(成员变量或局部变量)即称为常量。名称大写,且只能被赋值一次。
                        final标记的成员变量必须在声明时或在每个构造器中或代码块中显式赋值,然后才能使用。

        下面就从这三个方面来了解一下final关键字的基本用法。

修饰变量

        修饰变量是final用得最多的地方。

        final修饰变量表示常量,只能被赋值一次,赋值后值不再改变。

        在编写程序时,我们经常需要说明一个数据是不可变的,我们称之为常量。在java中,用final关键字修饰的变量,只能进行一次赋值操作,并且在生存期内不可以改变它的值。

        final修饰一个成员变量,必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数或构造代码块中对这个变量赋初值。

        static final 用来修饰属性:全局常量

public class Person {
    public final String DESC = "我是一个人类";

    public void setDesc(String desc){
        // Cannot assign a value to final variable 'DESC'
        // this.DESC = desc; // 编译报错
    }
}

        不过在针对基本类型和引用类型时,final关键字的效果存在细微差别。

        当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。

        我们来看下面的例子:

class Value {
    int v;
    public Value(int v) {
        this.v = v;
    }
}

public class FinalTest {
    //常量初始化方式1
    final int f1 = 1;
    
    //常量初始化方式2
    final int f2;
    public FinalTest() {
        f2 = 2;
    }
    
    // 常量初始化方式3
    final int f3;
    {
        f2 = 3;
    }

    public static void main(String[] args) {
        final int value1 = 1;
        // value1 = 4;
        final double value2;
        value2 = 2.0;
        
        final Value value3 = new Value(1);
        value3.v = 4;
    }
}

        上面的例子中,我们先来看一下main方法中的几个final修饰的数据,在给value1赋初始值之后,我们无法再对value1的值进行修改,final关键字起到了常量的作用。

        从value2我们可以看到,final修饰的变量可以不在声明时赋值,即可以先声明,后赋值。

        value3是一个引用变量,这里我们可以看到final修饰引用变量时,只是限定了引用变量的引用不可改变,即不能将value3再次引用另一个Value对象,但是引用的对象的值是可以改变的,从内存模型中我们看的更加清晰:

        上图中,final修饰的值用粗线条的边框表示它的值是不可改变的,我们知道引用变量的值实际上是它所引用的对象的地址,也就是说该地址的值是不可改变的,从而说明了为什么引用变量不可以改变引用对象。而实际引用的对象实际上是不受final关键字的影响的,所以它的值是可以改变的。

        另一方面,我们看到了用final修饰成员变量时的细微差别,因为final修饰的数据的值是不可改变的,所以我们必须确保在使用前就已经对成员变量赋值了。因此对于final修饰的成员变量,我们有且只有两个地方可以给它赋值,一个是声明该成员时赋值,另一个是在构造方法中赋值,在这两个地方我们必须给它们赋初始值。

        最后我们需要注意的一点是,同时使用static和final修饰的成员在内存中只占据一段不能改变的存储空间。

修饰方法参数

        前面我们可以看到,如果变量是我们自己创建的,那么使用final修饰表示我们只会给它赋值一次且不会改变变量的值。那么如果变量是作为参数传入的,我们怎么保证它的值不会改变呢?
    
        这就用到了final的第二种用法,即在我们编写方法时,可以在参数前面添加final关键字,它表示在整个方法中,我们不会(实际上是不能)改变参数的值:

public class FinalTest {

    /* ... */

    public void finalFunc(final int i, final Value value) {
        // i = 5; 不能改变i的值
        // v = new Value(); 不能改变v的值
        value.v = 5; // 可以改变引用对象的值
    }
}

修饰方法

下面这段话摘自《Java编程思想》第四版第143页:
         “使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“

        用final关键字修饰方法,它表示该方法不能被覆盖(重写)。这种使用方式主要是从设计的角度考虑,即明确告诉其他可能会继承该类的程序员,不希望他们去覆盖这个方法。这种方式我们很容易理解,然而,关于private和final关键字还有一点联系,这就是类中所有的private方法都隐式地指定为是final的,由于无法在类外使用private方法,所以也就无法覆盖它。

        因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的,也就是说子类是不能够存在和父类一模一样的方法的。

        final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)

public class B extends A {

    public static void main(String[] args) {

    }

    public void getName() {

    }
}

class A {

    /**
     * 因为private修饰,子类中不能继承到此方法,因此,子类中的getName方法是重新定义的、
     * 属于子类本身的方法,编译正常
     */
    private final void getName() {

    }

    /* 因为public修饰,子类可以继承到此方法,导致重写了父类的final方法,编译出错
    public final void getName() {

    }
    */
}

修饰类

        当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

        在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。

final总结

        上面我们讲解了final的四种用法,然而,对于第三种和第四种用法,我们却甚少使用。这不是没有道理的,从final的设计来讲,这两种用法甚至可以说是鸡肋,因为对于开发人员来讲,如果我们写的类被继承的越多,就说明我们写的类越有价值,越成功。

        即使是从设计的角度来讲,也没有必要将一个类设计为不可继承的。如果所有的方法均未被指定为final的话,它可能会更加有用。如此有用的类,我们很容易想到去继承和重写他们,然而,由于final的作用,导致我们对类的扩展受到了一些阻碍。

        final关键字是我们经常使用的关键字之一,它的用法有很多,但是并不是每一种用法都值得我们去广泛使用。它的主要用法有以下四种:    
                用来修饰数据,包括成员变量和局部变量,该变量只能被赋值一次且它的值无法被改变。对于成员变量来讲,我们必须在声明时或者构造方法中对它赋值;
                用来修饰方法参数,表示在变量的生存期中它的值不能被改变;
                修饰方法,表示该方法无法被重写;
                修饰类,表示该类无法被继承。

        上面的四种方法中,第三种和第四种方法需要谨慎使用,因为在大多数情况下,如果是仅仅为了一点设计上的考虑,我们并不需要使用final来修饰方法和类。

深入理解final关键字

        在了解了final关键字的基本用法之后,这一节我们来看一下final关键字容易混淆的地方。

1、final变量和普通变量有什么区别?

        当用final作用于类的成员变量时,成员变量必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。

        那么final变量和普通变量到底有何区别呢?下面请看一个例子:

public class Test {
    public static void main(String[] args)  {
        String a = "hello2";
        final String b = "hello";
        String d = "hello";
        String c = b + 2; // hello2
        String e = d + 2; // hello2
        System.out.println((a == c)); // true
        System.out.println((a == e)); // false
    }
}

        输出结果:true、false

        大家可以先想一下这道题的输出结果。为什么第一个比较结果为true,而第二个比较结果为fasle。这里面就是final变量和普通变量的区别了,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。因此在上面的一段代码中,由于变量b被final修饰,因此会被当做编译期常量,所以在使用到b的地方会直接将变量b 替换为它的值。而对于变量d的访问却需要在运行时通过链接来进行。不过要注意,只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化,比如下面的这段代码就不会进行优化:

        编译期常量和运行时常量:
                https://blog.csdn.net/qq_34802416/article/details/83548369

public class Test {
    public static void main(String[] args)  {
        String a = "hello2";
        final String b = getHello();
        String c = b + 2;
        System.out.println((a == c)); // false
    }

    public static String getHello() {
        return "hello";
    }
}

2、排错题

public class Something {
    public int addOne(final int x) {
        return ++x;
        // return x + 1;
    }
}

public class Something {
    public static void main(String[] args) {
        Other o = new Other();
        new Something().addOne(o);
    }
    public void addOne(final Other o) {
        // o = new Other();
        o.i++;
    }
}
class Other {
    public int i;
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是波哩个波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值