Java学习总结——对象的引用与传递

拾柒——对象的引用与传递

一、初步了解引用传递

引用传递也称为传地址,指的是在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。

方法调用时,若实际参数的引用——地址被传递给方法中相对应的形式参数,即形式参数和实际参数拥有相同的存储单元。在方法执行过程中,对形式参数的操作实际上就是对实际参数的操作,因此形式参数值的改变将会影响实际参数的值。

如果想进行引用传递分析,那么首先需要先清楚两块内存。

(1)堆内存:堆内存可以理解为一个对象的具体信息,每一个对象保存的只是属性信息,每一块堆内存的开辟都要通过关键字 new 来完成。

(2)栈内存:可以理解为一个整型变量( 只能够保存一个数值 ),其中保存的是一块( 只能保存一块 )堆内存空间的内存地址数值,为了方便理解,可以假设其保存的是对象的名字。

先通过对象的实例化操作了解内存分配。

举例:

//对象的实例化内存分配操作
class Book	//定义类 Book
{
	String title;
	double price;
	public void printInfo()
	{
		System.out.println("title:" + this.title);
		System.out.println("price:" + this.price);
	}
}

public class Test
{
	public static void main(String[] args)
	{
		Book book = null;	//声明对象
		book = new Book();	//实例化一个对象
		book.title = "重构";	//设置了类中的title属性
		book.price = 40.0;	//设置price属性
		book.printInfo();	//此处的方法使用对象调用,不是直接调用
	}
}

如果将 “ book = new Book(); ” 注释掉,此时将会出现 “ NullPointerException ” 错误提示。它表示空指向异常,指的是使用了一个未实例化的对象( 未开辟堆内存空间的对象 )进行了类中属性或方法的调用的时候就会出现这个异常信息。所以对象使用前一定要开辟堆内存空间。

在程序中一个关键字 new 产生一个对象,就开辟一个新空间,如果有两个关键字 new,就表示要开辟两个新内存空间,此处的两个对象都占有各自的内存空间,彼此的操作应该是独立的。

如下:

        Book bookA = new Book();    //实例化一个对象
        Book bookB = new Book();    //实例化一个对象

用关键字 new 实例化两个对象 bookA、bookB,两个对象各占一个独立的空间,彼此独立。因此,只要存在了关键字 new,不管何种情况下,都表示要开辟新的内存空间。

二、引用数据类型的传递

在 Java 中,类本身就是引用数据类型,而对于引用数据类型实际上就相当于其他语言之中的指针概念。

在 Java 中对于方法参数的传递,对象是传递引用,基本数据类型是传递值。

举例:

//在函数中传递基本数据类型
public class TestValue
{
	public static void change(int i,int j)	//交换参数的值
	{
		int temp = i;	//完成两个变量值的交换
		i = j;
		j = temp;
	}

	public static void main(String[] args)
	{
		int a = 3;
		int b = 4;
		change(a,b);	//调用方法
		System.out.println("a=" + a);
		System.out.println("b=" + b);
	}
}

引用数据类型的传递并没有改变数据本身的值,因为参数中传递的是基本类型 a 和 b 的备份,在函数中交换的也是那份备份的值而不是数据本身。

举例:

//传递引用数据类型
public class TestValue
{
	public static void change(int[] count)	//交换参数的值
	{
		count[0] = 0;
		System.out.println("在方法内部 count[0]=" + count[0]);
	}

	public static void main(String[] args)
	{
		int[] count = {1,2,3,4,5,6,7,8,9};
		System.out.println("在方法执行前 count[0]=" + count[0]);
		change(count);	//调用change方法
		System.out.println("在方法执行后 count[0]=" + count[0]);
	}
}

在方法中传递引用数据类型 int 数组,实际上传递的是其引用 count 的备份,它们都指向数组对象,在方法中可以改变数组对象的内容。即:对赋值的引用所调用的方法更改的是同一个对象。

举例:

//对象的传递引用
class Person
{
	String name;
	int age;
}

public class TestRefDemo
{
	public static void main(String[] args)
	{
		Person p1 = null;	//声明对象p1,此对象值为null,尚未实例化
		Person p2 = null;	//声明对象p1,此对象值为null,尚未实例化
		p1 = new Person();	//实例化对象p1
		p1.age = 20;
		p1.name = "张三";
		p2 = p1;	//将p1的引用赋给p2
		System.out.println("姓名:"+p2.name);
		System.out.println("年龄:"+p2.age);
		p1 = null;
	}
}

所谓的引用传递,指的是一块堆内存空间,同时被多个栈内存所指向。引用传递的核心认识:不同的栈内存如果指向了同一块堆内存之中,所做的修改将影响所有的栈内存。

每一块栈内存只能够保存一块堆内存的地址,但是反过来,一块堆内存可以同时被多个栈内存所指向,在这种情况下,如果要改变某一个栈内存的保存地址内容,则必须先断开已有的堆内存地址连接,才可以指向新堆内存空间,而如果一块堆内存空间没有任何的栈内存所指向的话,那么这块空间就将成为垃圾,所有的垃圾将等待被 JVM 中的 GC( Garbage Collector )进行不定期的收集,同时进行内存空间的释放。

三、引用传递与现实生活的联系

现在完成以下一种表示:人有一间房子,房子属于这个人。

如下:

//对象的引用
class Person
{
	private String no;
	private String name;
	private House house;	//表示一个人的房子
	
	public Person(String no,String name)
	{
		this.no = no;
		this.name = name;
	}
	
	public String getPersonInfo()
	{
		return "编号:" + this.no + ",姓名:" + this.name;
	}
	
	public void setHouse(House house)
	{
		this.house = house;	//引用传递
	}
	
	public House getHouse()
	{
		return this.house;
	}
}

class House
{
	private double area;
	private String address;
	private Person person;
	
	public House(double area,String address)
	{
		this.area = area;
		this.address = address;
	}
	
	public String getHouseInfo()
	{
		return "房子的面积:" + this.area + ",地址:" + this.address;
	}
	
	public void setPerson(Person person)
	{
		this.person = person;	//引用传递
	}
	
	public Person getPerson()
	{
		return this.person;
	}
}

public class TestHouse
{
	public static void main(String[] args)
	{
		Person per = new Person("001","张三");
		House ho = new House(180,"春熙路");
		per.setHouse(ho);	//人有一间房子
		ho.setPerson(per);	//房子属于一个人
		System.out.println(per.getPersonInfo());
		System.out.println(per.getHouse().getHouseInfo());
		System.out.println(ho.getPerson().getPersonInfo());
	}
}

四、集成设计模式

不同的事物类之间,都是依靠引用进行连接的。

那么再分析一下电脑的组成,假设有:主板、CPU、硬盘、内存、机箱、电源、显示器、键盘、鼠标。那么将电脑看成一个类,这些组件便是这个类中的各个对象。而这些组件便是这个类中的各个对象。而这些小的对象又可以单独作为一个类拥有自己的对象。

如下:

		class 主板
		{
		    private CPU 对象;
		    private 内存 对象;
		    private 硬盘 对象;
		}
		class CPU{}
		class 硬盘{}
		class 内存{}
		class 机箱{
		    private 电源 对象;
		    private 主板 对象;
		}
		class 电源{}
		class 显示器{}
		class 键盘{}
		class 鼠标{}
		class 电脑
		{
		    private 机箱 对象;
		    private 显示器 对象;
		    private 键盘 对象;
		    private 鼠标 对象;
		}

这种设计思路在开发模式上就称为集成设计模式即:将多个小的类集合到一个大的类之中,形成一个整体。

五、对象克隆

对象克隆指的就是将一个对象进行内容的复制,如果要想实现克隆操作,可以使用 Object 类之中定义的一个方法。

        protected Object clone() throws CloneNotSupportedException;

如果要想正常地实现克隆操作,那么对象所在的类必须实现 Cloneable 接口,但是这个接口里面并没有定义任何的办法,此接口属于标识接口,指的是一种能力的体现。

举例:

class Book implements Cloneable
{
	//可以被克隆
	private String title;
	private double price;
	
	public Book(String title,double price)
	{
		this.title = title;
		this.price = price;
	}
	
	public void setPrice(double price)
	{
		this.price = price;
	}
	
	public void setTitle(String title)
	{
		this.title = title;
	}

	public double getPrice()
	{
		return price;
	}
	
	public String getTitle()
	{
		return title;
	}
	
	@Override
	protected Object clone() throws CloneNotSupportedException
	{
		//重新定义了一次clone()
		return super.clone();
	}
	
	@Override
	public String toString()
	{
		return "title="+title+",price=" + price;
	}
}

public class TestCloneDemo
{
	public static void main(String[] args) throws Exception
	{
		Book bookA = new Book("重构",40.0);
		Book bookB = (Book)bookA.clone();	//对象克隆
		bookB.setPrice(60.0);
		System.out.println(bookA);
		System.out.println(bookB);
	}
}

对象的克隆其实际上就是新建一个内存空间将原有对象中的信息复制过来。

六、反射机制

反射机制是支撑各个框架开发的核心

1.认识反射

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

Java 反射机制主要提供了以下功能:在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法。

举例:

//根据对象找到类
import java.util.Date;
public class TestReflenction
{
	public static void main(String[] args) throws Exception
	{
		Date date = new Date();
		Class cls = date.getClass();	//通过Java反射机制得到类的包名
	}
}

在此之中使用的 getClass() 方法是由 Object 类所定义的:public final Class getClass(),此方法返回的对象类型为 Class,而 Class 是反射操作的源头。但是如果想取得 Class 类的实例化对象在 Java 之中有三种方式。

方式一:利用 Object 类的 getClass() 方法,但是要求必须先产生指定类的对象才可以,几乎不用。

        Date date = new Date();
        Classcls = date.getClass();
        System.out.println(cls);

方式二:利用 “ 类.class ” 的形式取得 Class 类的对象,在 Hibernate 上使用。

        Class cls = java.util.Date.class;
        System.out.println(cls);

方式三:利用 Class 类提供的一个方法完成,在系统架构中使用。

        Classcls = Class.forName("java.util.Date");
        System.out.println(cls);

那么取得了 Class 类的实例化对象到底有哪些用处呢?实际上在 Class 类之中提供有一个办法。

//根据对象找到类
class Book
{
	private String title;
	private double price;
	
	public void setPrice(double price)
	{
		this.price = price;
	}
	
	public void setTitle(String title)
	{
		this.title = title;
	}
	
	@Override
	public String toString(){
		return "名称:" + this.title + ",价格:" + this.price;
	}
}

public class TestReflection
{
	public static void main(String[] args)throws Exception
	{
		Classcls = Class.forName("Book");
		Book book = (Book)cls.newInstance();	//实例化一个对象
		book.setTitle("重构");
		book.setPrice(40.0);
		System.out.println(book);
	}
}

综上,如果要想实例化对象可以有两种方式,一种是通过关键字 new,另一种是通过反射机制完成。如果想要更好地观察出这两种方式的特点,最好的做法就是通过工厂设计模式完成验证。

举例:

//传统的工厂设计模式
interface Book
{
	public String getTitle();
}

class AllBook implements Book
{
	public String getTitle()
	{
		return "书";
	}
}

class Factory
{
	public static Book getInstance(String className)
	{
		if ("book".equals(className)) {
			return new AllBook();
		}
		return null;
	}
}

public class TestFactory
{
	public static void main(String[] args)
	{
		Book book = Factory.getInstance("book");	//实例化一个对象
		System.out.println(book.getTitle());
	}
}

这个时候,如果想要增加新的子类,则一定要修改工厂类。因为工厂类之中需要使用关键字 new 实例化,所以在这种情况下,发现关键字 new 依然会造成耦合,那么如果说现在使用的是反射机制呢?

如下:

//使用反射机制的工厂模式
//传统的工厂设计模式
interface Book
{
	public String getTitle();
}

class MathBook implements Book
{
	public String getTitle()
	{
		return "数学类书";
	}
}

class ComputerBook implements Book
{
	public String getTitle()
	{
		return "计算机类书";
	}
}

class Factory
{
	public static Book getInstance(String className)
	{
		Book book = null;
		try{
			book = (Book)Class.forName(className).newInstance();
		}catch(Exception e)
		{
			e.printStackTrace();
		}
		return book;
	}
}

public class TestFactory
{
	public static void main(String[] args) throws Exception
	{
		Book book = Factory.getInstance("Computerbook");	//实例化一个对象
		System.out.println(book.getTitle());
	}
}

不管有多少个子类,工厂类都可以使用,这就是反射机制所带来的的好处,而在日后的开发之中,如果发现有时候需要写出完成的 “ 包.类 ”,就表示此处使用了反射机制。

2.反射的其他操作

如果此时类中没有提供无参构造方法,只提供了有参构造方法,则就必须明确地调用指定的构造方法才可以通过反射实例化对象。

取得指定构造方法如下:

        public ConstructorgetConstructor(Class…parameterTypes) throws NoSuchMethodException,SecurityException;

在 Constructor 类之中提供有一个实例化对象方法。

        public T newInstance(Object…initargs) throws
        InstantiationException,IllegalAccessException,
        IllegalArgumentException,InvocationTargetException

举例:

//调用构造方法取得实例化对象
import java.lang.reflect.Constructor;
class Book
{
	private String title;
	private double price;
	
	public Book(String title,double price)
	{
		this.title = title;
		this.price = price;
	}
	
	@Override
	public String toString()
	{
		return "名称:" + this.title + ",价格:" + this.price;
	}
}

public class TestInstance
{
	public static void main(String[] args) throws Exception
	{
		Class cls = Class.forName("Book");
		Constructor cons = cls.getConstructor(String.class,double.class);
		Book book = (Book)cons.newInstance("重构",40.0);
		System.out.println(book);
	}
}

此类中还是提供无参构造方法会更加方便一些,这一点就是在简单 Java 类之中要求提供无参构造的关键因素。

在之前针对属性的操作明确给出了要求,利用 setter、getter 设置和取得,而且对于 setter、getter 要求其必须按照指定的格式编写。而之所以存在这样的要求,也是因为反射机制的原因。

此时可以使用 Class 类的如下方法取得方法的对象。

        public Method getMethod(String name,Class… parameterTypes)
        throws NoSuchMethodException,SecurityException;

取得了 Method 类的对象之后可以利用以下方法进行方法的反射调用。

        public Object invoke(Object obj,Object… args) throws IllegalAccessException,
        IllegalArgumentException,InvocationTargetException;

举例:

//setter、getter的使用
import java.lang.reflect.Method;
class Book
{
	private String title;

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}
}

public class TestReflection
{
	public static void main(String[] args) throws Exception
	{
		String fileName = "title";	//要操作的属性
		String titleValue = "重构";
		Class cls = Class.forName("Book");
		Object obj = cls.newInstance();	//产生对象可以分配堆内存
		Method setMethod = cls.getMethod("set" + initcap(filedName), String.class);
		Method getMethod = cls.getMethod("get" + initcap(filedName));
		setMethod.invoke(obj, titleValue);	//对象 .setTitle()调用
		System.out.println(getMethod.invoke(obj)); 	//对象 .getTitle()调用
	}
	
	public static String initcap(String str)
	{
		return str.substring(0,1).toUpperCase() + str.substring(1);
	}
}

有了反射之后,只要有 Object 对象,以及要操作的属性名成就可以直接利用反射完成了,这个就是 setter、getter 命名规则的定义由来。

框架开发原理 = 反射机制 + XML 解析。

七、本文注意事项

1.对象引用相等

引用相等:即指引用到堆上同一个对象的两个引用是相等的。

若要判断两个对象内容相同,需要覆写方法 equals()。

2.Java 中的垃圾回收机制

每一块栈内存只能够保存一块堆内存的地址,但是反过来,一块堆内存可以同时被多个栈内存所指向,在这种情况下,如果要改变某一个栈内存的保存地址内容,则必须先断开已有的堆内存地址连接,才可以指向新堆内存空间,而如果一块堆内存空间没有任何的栈内存所指向的话,那么这块空间就将成为垃圾,所有的垃圾将等待被 JVM 中的 GC( Garbage Collector )进行不定期的收集,同时进行内存空间的释放。

虽然 Java 之中提供了垃圾的自动回收机制,但是考虑到性能的问题,还是建议操作的过程之中尽量少产生垃圾为好。

你可能感兴趣的:(Java,Java)