一.博客摘抄
1.Java的String和StringBuffer和StringBuilder详解
3.Java的包装类
11.动态代理
12.多线程/线程池/同步
二.java参数传递:java语言中,将实参的值传递给形参时,到底传递的是什么?
我们知道,实参和形参在栈上分别有一个内存地址,内存里面存放的内容即是他们的值,比如:
int number = 10;//在栈的内存上开辟一块空间来存放number,具体的内存地址值由jvm来分配和管理,比如在0x12345678的内存地址上存放number的值10
Person person = new Person(“zhangsan”);//在栈上开辟一块内存存放person,在堆上开辟一块内存存放zhangsan对象,其中栈上的person内存地址里面存放zhangsan对象在堆上的地址值,即栈上的person引用着堆上的zhangsan对象。
在参数传递过程中,实参将其自己的存储内容传给了形参的内存存储中。接下来我们看三个例子来验证下。
... ...
//定义了一个改变参数值的函数
public static void changeValue(int x) {
x = x *2;
}
... ...
//调用该函数
int num = 5;
System.out.println(num);
changeValue(num);
System.out.println(num);
... ...
答案显而易见,调用函数changeValue()前后num的值都没有改变。在该例子中changeValue函数的形参x的值是由实参num的内存存储值传过去的,即将5赋值给了形参x的内存存储中。
在看一个例子
... ...
class person {
public static String name = "Jack";
... ...
}
... ...
//定义一个改变对象属性的方法
public static void changeName(Person p) {
p.name = "Rose";
}
... ...
public static void main(String[] args) {
//定义一个Person对象,person是这个对象的引用
Person person = new Person();
//先显示这个对象的name属性
System.out.println(person.name);
//调用changeName(Person p)方法
changeName(person);
//再显示这个对象的name属性,看是否发生了变化
System.out.println(person.name);
}
输出的答案:
第一次显示:“Jack”
第二次显示:“Rose”
在该例子中,位于栈上的形成p的值是由同样位于栈上的实参person给赋值的,由于实参person栈空间里面存放的是堆空间Person的对象引用,即实参person里面存放的是堆空间Person的地址,在发生实参向形参传递赋值的过程时,实参中将该地址传给了形参,即形参的内存中也存放的是堆空间里面的Person对象的地址。因此,在changeName中对形参的Person对象的name属性修改时会反应到实参中。在看一个例子
public class Example {
String str = new String("good");
char[] ch = { 'a', 'b', 'c' };
public static void main(String args[]) {
Example ex = new Example();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.print(ex.ch);
}
public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'g';
}
}
分析下这个Demo中为什么 str没被改变,但是ch[0]却改变了。
在main中new了一个Example对象,ex.str指向了该对象中的String成员对象(其值为"good"),然后调用change函数,将ex.str作为实参传递给形参str,此时ex.str和str都是引用,传递的过程是将ex.str引用的值传递给str,那么str也将指向同一字符串对象("good"),
随后进入change函数,str = "test ok"; 该语句将在JVM的字符串常量池中创建一个新的字符串常量("test ok")对象,赋值给str的其实是新字符串常量对象的地址值。好了,str的值改变了,而str本身是String类型的一个对象引用,那么该引用现在指向了新的字符串对象。(注意,此处跟上面例子的区别,上面例子中形参始终指向实参,两者的地址是一样的,形参从未发生指向的改变,改变的只是地址里面的属性,而此处是将指向/地址发生了改变) 而我们外层的Example对象仍然由ex这个引用指向,并且在ex内部还有一个ex.str的引用,它仍然指向"good"字符串,一直没有改变,当你使用ex.str打印时,就显示了"good"没有变化。
以上就是str没有改变的原因,至于ch[0]是数组,跟上面例子中的Person一样,修改的是地址里面的值。
通过上面的三个例子,我们可以得出结论,实参向形参传递的是实参内存存储单元里面的值(该值即可能是基本数据类型也可能是引用类型,如果是引用类型的话则是一个地址)。如果实参里面直接存放的是基本数据类型的话就直接拷贝一份赋值给形参,如果实参里面存放的是堆空间引用类型对象的话,自己存储单元里面存放的就是地址,因此拷贝一份地址传给形参。从这个角度来说的话,无论实参是基本类型还是引用类型,都是将自己存储单元的值直接拷贝了一份给形参,这也解释了java中参数传递都是值传递而没有引用传递的原因。
三.C语言参数传递
接来下我们在来看看C语言中,实参向形参传递时是否跟java一样?
typedef struct mystruct mystruct;
struct mystruct
{
int i;
char c;
int arrary[2];
char *pChar;
};
void pointTest(mystruct *pmystruct)
{
int a = 100;
int *ap = &a;
printf("%p\n",&a); //输出:002AF744
printf("%p\n",ap); //输出:002AF744
printf("%d\n",*ap); //输出:100
printf("%p\n",&ap); //输出:002AF738
printf("%p\n",&*ap);//输出:002AF744
printf("&pmystruct address = %p\n", &pmystruct);
printf(" pmystruct address = %p\n", pmystruct);
scanf("%d");
}
int _tmain(int argc, _TCHAR* argv[])
{
mystruct temp;
printf("temp address = %p\n", &temp);
pointTest(&temp);//测试指针本身地址、指针指向地址及指向的内容
return 0;
}
输出结果:
temp address = 0041F770
0041F688
0041F688
100
0041F67C
0041F688
&pmystruct address = 0041F69C
pmystruct address = 0041F770
分析:
1. printf("%d\n",&a);//输出:002AF744
这一句输出的是变量a的地址,毋庸置疑。
2. printf("%d\n",ap);//输出:002AF744
这一句是输出的是指针的值,也就是说指针的值是指针所指向的变量的地址
3. printf("%d\n",*ap);//输出:100
在指针变量的前面加了一个*号,不加星号的ap指针是指向变量a的地址,而加了*真变成了指针ap所指向的变量a的内容,所以,我们可以理解为*号是获取指针变量所指向的地址所存放的内容的操作。
4. printf("%d\n",&ap);//输出:002AF738
这一句(同1)是取得指针变量ap的地址
5. printf("%d\n",&*ap);//输出:002AF744
这一句根据第3点的分析,*ap指向的是变量a的内容,而&*ap即是获取变量a的内容的地址,即是变量a的地址所以输出内容同(1)。
小结一:
C语言中在发生有参函数调用时,实参变量与形参变量之间的数据都是单向的“值传递”方式。包括指针变量和数组名作参数的情况。C语言要求函数的实参要有确定的值,在函数调用时给形参分配相应的内存单元,同时将实参的“值”赋(复制)给形参,实现数据从实参到形参的传递(‘值传递’方式)。因为是复制,所以在操作副本(形参)过程中不会影响到原本(实参)内容。
作为函数实参的量包括常量、变量和表达式,其中变量又包括简单变量、数组元素、数组名、指针变量等。不同类
型变量作参数实现的数据传递方式相同,效果不同。所谓方式相同即都是参数间数据单向的“值传递”,效果不同
是指被调函数能否改变主调函数中变量的值。
四.使用静态内部类实现延迟加载单例模式
单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类就称为单例类。
单例的创建一般分为懒汉式,饿汉式,双重锁检查,枚举等,其中双重锁检查只能在jdk1.5以后才能有效,1.5以前在java对象模型中的无序写问题不能保证。
下面就介绍使用内部类的机制来巧妙实现懒汉式单例模式的实现,这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识(jvm确保了一个类只会加载一次),很巧妙地同时实现了延迟加载和线程安全,同时不受jdk版本的影响。
内部类简单介绍
内部类分为对象级别和类级别,类级内部类指的是,有static修饰的成员变量的内部类。如果没有static修饰的成员变量的内部类被称为对象级内部类。类级内部类相当于其外部类的static成员,它的对象与外部类对象间不存在依赖关系,相互独立,因此可直接创建,而对象级内部类的实例,是必须绑定在外部对象实例上的。类级内部类只有在第一次被使用的时候才被会装载。
要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性,如饿汉式单例,这种实现方式,会在类装载的时候就初始化对象,有可能浪费一定的内存(假设你不需要的话),有一种方法能够让类装载的时候不去初始化对象,就是采用类级内部类,在这个类级内部类里面去创建对象实例。
代码如下:
/**
* 利用静态内部类实现懒汉模式/线程安全的单例模式。
* 线程安全:静态内部类只有在通过getInstance()方法调用时才会加载而且也只加载一次,而这就是静态内部类实现线程安全的单例模式的理论基础。
* jvm机制中规范了一个类只会加载一次。
*
* 参考 http://blog.youkuaiyun.com/liushuijinger/article/details/9069801
* Created by ZhengGuangGuo on 2017/12/19 10:51.
*/
public class Singleton {
private Singleton() {}
public Singleton getInstance() {
return SingletonHold.instance;
}
public static class SingletonHold {
private final static Singleton instance = new Singleton();
}
}
//双重非空校验+锁+volatile实现线程安全
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton() {
}
public Singleton getInstance() {
//避免每次都加锁synchronized校验影响性能,因此采用双重非空校验
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
} //end synchronized
}
return singleton;
}
}
/**
* 饿汉式
* 利用jvm类加载机制实现线程安全的单例模式,一个类当且仅当加载一次
*/
public class Singleton {
//单例变量,static, 在类加载时进行初始化一次,保证线程安全
private static Singleton instance = new Singleton();
//私有化的构造方法,保证外部的类不能通过构造器来实例化。
private Singleton() {
}
//获取单例对象实例
public static Singleton getInstance() {
System.out.println("我是饿汉式单例!");
return instance;
}
}
当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,内部类SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。