Java中的一些基础知识

1.1 不可变类

不可变类(Immutable class)是指当一个对象一旦被创建出来,在其整个生命周期中,它的成员变量就不能被修改。在Java类库中,所有基本类型的包装类都是不可变类,如Integer、Float等,此外,String也是不可变类。

以下代码是修改了String类型的值吗?

public class Test{
    public static void main(String[] args){
        String s = "Hello";
        s+=" world";
        System.out.println(s);
    }
}
复制代码

实际上,s+=" world"并没有改变s所指向的对象,而是指向了另一个String类型的对象,该对象的内容为"Hello world"。原来的字符串常量"Hello"还存在内存中,并没有改变。

通常来讲,要创建一个不可变类需要遵循下面五条原则:

  1. 类中所有的成员变量被private修饰
  2. 类中没有写或者修改成员变量的方法,例如:setxxx。只提供构造函数,一次生成,永不改变。
  3. 确保类中的所有方法不会被子类覆盖,可以通过把类定义为final或者把类中的方法定义为final来达到这个目的。
  4. 如果一个类成员不是不可变量,那么在成员初始化或者使用get方法获取该成员变量是需要通过clone方法,来确保类的不可变性。
  5. 如果有必要,可以通过覆盖Object类的equals()方法和hashCode()方法。

由于类的不可变性,在创建对象的时候就需要初始化所有的成员变量,因此最好提供一个带参数的构造函数来初始化这些成员变量。

一个错误示范:

public class ImmutableClass {
    private Date d;
    public ImmutableClass(Date d){
        this.d = d;
    }
    public void printState(){
        System.out.println(d);
    }
}
​
public class Test {
    public static void main(String[] args) {
        Date d = new Date();
        ImmutableClass immu = new ImmutableClass(d);
        immu.printState();
        d.setMonth(5);
        immu.printState();
    }
}
复制代码

image.png 由于Date的对象的状态是可以被改变的,而ImmutableClass保存了Date类型对象的引用,当被引用的对象的状态改变的时候会导致ImmutableClass对象状态的改变。

正确的实现方式为:

public class ImmutableClass {
    private Date d;
    public ImmutableClass(Date d){
        // this.d = d;
        this.d = (Date)d.clone();// 解除了引用关系
    }
    public void printState(){
        System.out.println(d);
    }
}
​
public class Test {
    public static void main(String[] args) {
        Date d = new Date();
        ImmutableClass immu = new ImmutableClass(d);
        immu.printState();
        d.setMonth(5);
        immu.printState();
    }
}
复制代码

image.png

不可变类的优缺点:

  • 优:不可变类具有使用简单、线程安全、节省内存等优点
  • 缺:不可变的对象会因为值的不同而产生新的对象,导致无法预料的问题

Question:

对于一些敏感的数据(例如密码),为什么使用字符串数组存储比使用String安全?

在Java中,String是不可变类,被存储在常量字符串池中,从而实现了字符串的共享,减少了内存开支。正因为如此,一旦一个String类型的字符串被创建出来,这个字符串就会存在于常量池中,知道被垃圾回收器回收为止。

因此,即使这个字符串(比如密码)不再被使用,仍然会在内存中存在一段时间(只有垃圾回收器才会回收这块内容,程序员无法直接回收字符串)。此时有权限访问memory dump(存储器转储)的程序都可能会访问到这个字符串,从而把敏感的数据暴露出去,这是一个非常大的安全隐患。

如果使用字符数组,一旦程序不再使用这个数据,程序员可以把字符数组的内容设置为空,此时这个数据在内存中就不存在。

也就是说,跟String相比,使用字符数组,程序员对数据的生命周期有更好的控制,增强安全性。

1.2 "= ="、equals和hashcode

1.2.1 "= ="

“==”运算符用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,

  • 要比较两个基本类型的数据或两个引用变量是否相等,只能使用“==”运算符.

  • 要比较两个引用变量是否指向同一块内存,可以使用“= =”

  • 要比较两个引用变量的对象内容是否相等,无法使用"= ="

1.2.2 equals

Object类中定义的equals(Object)方法是直接使用“==”运算符比较的两个对象,所以在没有覆盖equals(Object)方法的情况下,equals(Object)与“==”运算符一样,比较的是引用。

相比“==”运算符,因为equals(Object)方法的特殊之处就在于它可以被覆盖,所以可以通过覆盖的方法让它不是比较引用而是比较数据内容

1.2.3 hashCode

Object类中的hashCode()方法返回对象在内存中地址转换成的一个int值,所以如果没有重写hashCode()方法,那么任何对象的hashCode()方法都是不相等的。

equals方法和hashCode方法的区别?

  • 一般来讲,equals方法是给用户调用的,如果需要判断两个对象是否相等,可以重写equals方法,然后在代码中调用,即可判断是否相等
  • 对于hashCode()方法,用户一般不会去调用它。多用在hashmap、hashset等需要判断元素是否重复的地方。

一般在覆盖equals方法的同时也要覆盖hashCode()方法,否则会违反Object.hashCode的通用约定,导致该类无法与所有基于hash值的集合类结合正常运行。

1.3 值传递和引用传递

按值传递指的是在方法调用时,传递的参数是实参值的拷贝按引用传递指的是在方法调用时,传递的参数是实参的引用,也可以理解为实参所对应的内存空间的地址

public class Test {

    public static void testPassParameter(StringBuffer s1,int n){
        s1.append(" World");
        n = 8;
    }
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer("Hello");
        int n = 1;
        testPassParameter(sb,n);
        System.out.println(sb);
        System.out.println(n);
    }
}
复制代码

image.png

从运行结果看,int作为参数的时候,对形参的修改不会影响实参,对于StringBuffer类型的参数,对形参的修改影响到了实参。可以理解为:基本类型的参数时按值传递,引用类型的参数时引用传递。

实际上,Java语言中的引用传递还是值传递(传递的是地址的值)。

1.4 Java关键字

1.4.1 static

static关键字主要有两个作用:

1、为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。

2、实现某个方法或属性与类而不是对象关联在一起

也就是说,在不创建对象的情况下就可以通过类来直接使用类的方法或者属性

static可修饰的元素

  • 变量:静态变量,可以跨越代码块访问
  • 方法:静态方法,可以跨越代码块访问
  • 代码块:静态代码块,只能定义在类定义下,在类被加载时执行
  • 内部类:静态内部类,该类定义可以有外部类名引用
  • 导入包:静态导入包,导入指定的static变量

加载时机

static,静态,表示随着类的加载而加载,不会重复加载,执行顺序在main方法执行。在JVM内存中,static修饰的变量存在于方法区中。

1.4.2 final

final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承(不能再派生处新的子类)。

  • final属性:被final修饰的变量不可变,不可变又两重含义,一是引用不可变,二是对象不可变。final指的是哪种含义?

    public class Test {
        
        public static void main(String[] args) {
            final StringBuffer sb = new StringBuffer("Hello");
            sb = new StringBuffer("Hello World!");
        }
    }
    复制代码

编译期间错误

public class Test {
​
    public static void main(String[] args) {
        final StringBuffer sb = new StringBuffer("Hello");
        sb.append(" World!");
        System.out.println(sb);
    }
}
复制代码

运行结果为:Hello World!

可以看出,final指的是引用不可变,只能之现象初始时指向的那个对象,而不关心对象内容的变化。

final变量初始化的几个方式:

  1. 在定义的时候初始化
  2. final成员变量可以在初始化块中初始化,但不可在静态初始化块中初始化
  3. 静态final成员变量可以在静态初始化块中初始化;
  4. 在类的构造器中初始化,但静态final成员变量不可以在构造方法中初始化
  • final方法:当一个方法声明为final时,该方法不允许任何子类重写这个方法,但子类仍然可以使用这个方法。
  • final参数:用来表示这个参数再这个方法内部不允许被修改
  • final类:当一个类被声明为final时,此类不能被继承,所有方法都不能被重写。但并不表示final类的成员变量也是不可变的,要想做到final类的成员变量不可改变,必须给成员变量增加final修饰。值得注意的是,一个类不能既被声明为abstract,又被声明为final。

Question:为什么匿名内部类只能使用成员变量或者被final修饰的局部变量?

是因为匿名内部类的生存期可能比一般的局部变量更久

例如一个Runable的实现体,有可能在数秒之后才被调用,而它的外部方法体已经随着代码执行完毕而消亡了,之前定义在外部方法体内的变量随着方法区内存的回收也一起消亡了。

被final修饰的局部变量在匿名内部类中有一个引用的副本,由于它本身不可被修改引用,所以可以在开发期认为final局部变量和内部类的引用副本是同一个引用。

1.4.3 transient

Java的serialization提供了一种持久化对象实例的机制。当持久化一个对象时,可能并不想持久化所有的属性。对于这种情况,可以通过在属性前加上关键字transient来实现。

在分布式环境下,当进行远程通信时,数据以二进制序列的形式在网络上传送。序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。

如何实现序列化?

所有要实现序列化的类都必须实现Serializable接口,Serializable接口位于java.lang包中,它里面没有包含任何方法。使用一个输出流(例如FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,紧接着,使用该对象的writeObject(Object obj)方法就可以将obj对象写出(即保存其状态),要恢复的时候可以使用其对应的输入流。

序列化的特点:

1)如果一个类能被序列化,那么它的子类也能够被序列化

2)由于static(静态)代表类的成员,transient(Java语言关键字,如果用transient声明一个实例变量,那么当对象存储时,它的值不需要维持)代表对象的临时数据,因此,被声明为这两种类型的数据成员是不能够被序列化的

3)子类实现了Serializable接口,父类没有,父类中的属性不能序列化,但是子类中的属性仍能正确序列化。

猜你喜欢

转载自juejin.im/post/7042614492160589838
今日推荐