java中关于找对象的一些方法

1.new一个对象

Bh bh1= new Bh();
Bh bh2= new Bh("bhgeek", 18, 65.0f);

通过new的方式,我们可以调用类的无参或者有参构造来实例化出一个对象。
new一个对象的具体流程:
markdowm

  1. new一个对象时候,JVM首先会检查对象是否被加载过,如果没有就要执行对应加载过程
  2. 声明类型引用,比如Bh bh1= new Bh()就会声明一个Bh类型的bh
  3. 类加载完成后,JVM就会在堆上分配内存
  4. 属性初始化,int的初始化是0;对象是null
  5. JVM进行对象头的设置,这里面就主要包括对象的运行时数据(比如Hash码、分代年龄、锁状态标志、锁指针、偏向线程ID、偏向时间戳等)以及类型指针(JVM通过该类型指针来确定该对象是哪个类的实例),这个我不怎么懂。
  6. 属性的显示初始化,当你手动给某个字段赋值时,就在这阶段初始化上去
  7. 最后调用类的构造方法来进行构造方法内描述的初始化动作

2.反射出一个对象

只要能拿到类的Class对象,就可以通过反射来创造实例对象
一般来说,拿到Class对象有三种方式:

  1. 类名.Class
  2. 对象名.getClass
  3. Class.forName(全限定类名)

有了Class对象之后,接下来就可以用newInstance()方法来创建一个对象
例如:

Bh bh= (Bh) Class.forName( "cn.hai.article.obj.Bh" ).newInstance();
Bh bh1= Bh.class.newInstance();

但是,这种方式有局限性,只能使用无参构造方法来创建

更进一步是通过java.lang.relect.ConstructornewInstance()来创建对象,这个方法可以指定某个构造器来创建对象
当有Class对象后,可以通过getDeclaredConstructors()函数来获取类的所有构造参数列表,就可以调用对应函数,Like this:

Constructor<?>[] constructors = Bh.class.getDeclaredConstructors();
Bh bh1= (Bh) constructors[0].newInstance(); 
Bh bh2= (Bh) constructors[1].newInstance( "bhgeek", 18, 65.1f );

也可以通过指定某个类的构造参数类型来精确控制,Like this:

Constructor<?>[] constructors = Bh.class.getDeclaredConstructor(String.class, Integer.class, Float.class );
Bh bh2= (Bh) constructors.newInstance( "bhgeek", 18, 65.1f );

3.克隆出一个对象

赋值 vs 浅拷贝 vs 深拷贝

1.赋值

Bh bh= new Bh();
Bh name = bhgeek;

赋值只是一层引用关系,没有生成新的实际对象

2.浅拷贝

浅拷贝的值类型的字段会复制一份,而引用类型的字段拷贝的仅仅是引用地址,而该引用地址指向的实际对象空间其实只有一份。
markdown

3.深拷贝

深拷贝相较于上面所示的浅拷贝,除了值类型字段会复制一份,引用类型字段所指向的对象,会在内存中也创建一个副本,就像这个样子:
markdown

4.代码实现

浅拷贝的典型实现方式是:让被复制对象的类实现Cloneable接口,并重写clone()方法即可。

深拷贝需覆写 clone()方法实现引用对象的深度遍历式拷贝,进行地毯式搜索。
如果想实现深拷贝,首先需要对更深一层次的引用类Major做改造,让其也实现Cloneable接口并重写clone()方法:


public class Major implements Cloneable {

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    // ... 其他省略 ...
}

其次我们还需要在顶层的调用类中重写clone方法,来调用引用类型字段的clone()方法实现深度拷贝,对应到本文那就是Student类:


public class Student implements Cloneable {

    @Override
    public Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.major = (Major) major.clone(); // 重要!!!
        return student;
    }
    
    // ... 其他省略 ...
}

4.反序列化一个对象

1.序列化的作用

序列化的原本意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播。
序列化:把Java对象转换为字节序列。
反序列化:把字节序列恢复为原先的Java对象。

2.对象如何进行序列化

举个例子,假如我们要对Student类对象序列化到一个名为student.txt的文本文件中,然后再通过文本文件反序列化成Student类对象:

1、Student的定义

public class Student implements Serializable {

    private String name;
    private Integer age;
    private Integer score;
    
    @Override
    public String toString() {
        return "Student:" + '\n' +
        "name = " + this.name + '\n' +
        "age = " + this.age + '\n' +
        "score = " + this.score + '\n'
        ;
    }
    
    // ... 其他省略 ...
}

2、序列化

public static void serialize(  ) throws IOException {

    Student student = new Student();
    student.setName("bhgeek");
    student.setAge( 18 );
    student.setScore( 1000 );

    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
    
    System.out.println("序列化成功!已经生成student.txt文件");
    System.out.println("==============================================");
}

3、反序列化

public static void deserialize(  ) throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    
    System.out.println("反序列化结果为:");
    System.out.println( student );
}

4、运行结果

序列化成功!已经生成student.txt文件
==============================================
反序列化结果为:
Student:
name = bhgeek
age = 18
score = 1000

3.Serializable接口有何用?

上面在定义Student类时,实现了一个Serializable接口,然而当我们点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!
markdowm
试想,如果上面在定义Student类时忘了加 implements Serializable时会发生什么呢?

实验结果是:此时的程序运行会报错,并抛出NotSerializableException异常:
markdowm
我们按照错误提示,由源码一直跟到ObjectOutputStreamwriteObject0方法底层一看,才恍然大悟:
markdowm
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!
Serializable接口也仅仅只是做一个标记用!!!
它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。

4.serialVersionUID号有何用?

private static final long serialVersionUID = -4392658638228508589L;

1、serialVersionUID 是序列化前后的唯一标识符
2、默认如果没有人为显式定义过serialVersionUID ,那编译器会为它自动声明一个!

如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID 的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID ,一旦更改了类的结构或者信息,则类的serialVersionUID 也会跟着变化!

所以,为了serialVersionUID 的确定性,写代码时还是建议,凡是implements Serializable的类,都最好人为显式地为它声明一个serialVersionUID 明确值!

4.两种特殊情况

1、凡是被static修饰的字段是不会被序列化的
2、凡是被transient修饰符修饰的字段也是不会被序列化的
对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。
对于第二点,如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。

4.序列化的受控和加强

序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了
我们可以自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。
以上面的Student类为例,一般来说学生的成绩应该在0 ~ 100之间,我们为了防止学生的考试成绩在反序列化时被别人篡改成一个奇葩值,我们可以自行编写readObject()函数用于反序列化的控制:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {

    // 调用默认的反序列化函数
    objectInputStream.defaultReadObject();

    // 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作!
    if( 0 > score || 100 < score ) {
        throw new IllegalArgumentException("学生分数只能在0到100之间!");
    }
}

比如我故意将学生的分数改为101,此时反序列化立马终止并且报错:
markdowm


5.Unsafe

我们都知道写Java代码,很少会去操作位于底层的一些资源,比如内存等这些。而位于sun.misc.Unsafe()包路径下的Unsafe类提供了一种直接访问系统资源的途径和方法,可以进行一些底层的操作。比如借助Unsafe我们就可以分配内存、创建对象、释放内存、定位对象某个字段的内存位置甚至并修改它等等。

可见这玩意误用时的破坏力是很大的,所以一般也都是受控使用的。业务代码里很少能看到它的身影,但是JDK内部的一些诸如io、nio、juc等包中的代码里还是有不少关于它的身影存在的。
Unsafe类中有一个allocateInstance()方法,通过其就可以创建一个对象。为此我们只需要获取到一个Unsafe类的实例对象,我们自然就可以调用allocateInstance()来创建对象了。

那如何才能获取到一个readObject()Unsafe类的实例对象呢?

大致瞅一眼Unsafe类的源码我们就会发现,它是一个单例类,其构造方法是私有的,所以直接构造是不太现实了:

public final class Unsafe {

    private static final Unsafe theUnsafe;

    // ... 省略 ...

    private static native void registerNatives();

    private Unsafe() {
    }
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
    
    // ... 省略 ...
    
}

而且获取单例对象的入口函数getUnsafe()上也做了特殊标记,意思是只能从引导加载的类才可以调用该方法,这意味着该方法也是供JVM内部使用的,外部代码直接使用会报类似这样的异常:

Exception in thread "main" java.lang.SecurityException: Unsafe

走投无路,我们只能再次重拾强大的反射机制来创建Unsafe类的实例了:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

然后接下来我们就可以愉快地利用它来创建对象了:

Bh bh = (bh) unsafe.allocateInstance( bh.class );

6.对象的隐式创建

当然除了上述这几种显式地对象创建场景之外,还有一些我们并没有进行手动对象创建的隐式场景,举几个常见例子。

1.Class类实例隐式创建

我们都知道JVM虚拟机在加载一个类的时候,也都会创建一个类对应的Class实例对象,很明显这一过程是JVM偷偷地背着我们干的。

2.字符串隐式对象创建

典型的,比如定义一个String类型的字面变量时,就可能会引起一个新的String对象的创建,就像这样:

String name = "bhgeek";

还常见的比如String的+号连接符也会隐式地导致新String对象的创建等:

String str = str1 + str2;

3.自动装箱机制

Integer bhGeekAge= 18;

其触发的自动装箱机制就会导致一个新的包装类型的对象在后台被隐式地创建出来。

4.函数可变参数

比如像下面这样,当我们使用可变参数语法int... nums来描述一个函数的入参时:

public double avg( int... nums ) {
    double sum = 0;
    int length = nums.length;
    for (int i = 0; i<length; ++i) {
        sum += nums[i];
    }
    return sum/length;
}

从表面上看,函数的调用处可以传入各种离散参数参与计算:

avg( 2, 2, 4 );
avg( 2, 2, 4, 4 );
avg( 2, 2, 4, 4, 5, 6 );

而背地里可能会隐式地产生一个对应的数组对象进行计算。

posted @ 2022-05-16 16:49  bhgeek  阅读(113)  评论(0)    收藏  举报