java常量与变量
一,java常量
在Java语言中,主要是利用final关键字(在Java类中灵活使用Static关键字)来定义常量。
当常量被设定后,一般情况下就不允许再进行更改。如可以利用如下的形式来定义一个常量:final double PI=3.1315。
在定义这个常量时,需要注意如下内容:
一是常量在定义的时候,就需要对常量进行初始化。也就是说,必须要在常量声明时对其进行初始化。这跟局部变量或者成员变量不同。当在常量定义的时候初始化过后,在应用程序中就无法再次对这个常量进行赋值。如果强行赋值的话,数据库会跳出错误信息,并拒绝接受这一个新的值。(接口中定义的常量的访问方法)
二是final关键字使用的范围。这个final关键字不仅可以用来修饰基本数据类型的常量,还可以用来修饰对象的引用或者方法。如数组就是一个对象引用。为此可以使用final关键字来定义一个常量的数组。这就是Java语言中一个很大的特色。一旦一个数组对象被final关键字设置为常量数组之后,它只能够恒定的指向一个数组对象,无法将其改变指向另外一个对象,也无法更改数组(有序数组的插入方法可使用的二分查找算法)中的值。
三是需要注意常量的命名规则。不同的语言,在定义变量或者常量的时候,都有自己一套编码规则。这主要是为了提高代码的共享程度与提高代码的易读性。在Java语言中,定义常量的时候,也有自己的一套规则。如在给常量取名的时候,一般都用大写字符。在Java语言中,大小写字符是敏感的。之所以采用大写字符,主要是跟变量进行区分。虽然说给常量取名时采用小写字符,也不会有语法上的错误。但是,为了在编写代码时能够一目了然的判断变量与常量,最好还是能够将常量设置为大写字符。另外,在常量中,往往通过下划线来分隔不同的字符。而不想对象名或者类名那样,通过首字符大写的方式来进行分隔。这些规则虽然不是强制性的规则,但是为了提高代码友好性,方便开发团队中的其他成员阅读,这些规则还是需要遵守的。没有规矩,不成方圆。
总之,Java开发人员需要注意,被定义为final的常量需要采用大写字母命名,并且中间最好使用下划线作为分隔符来进行连接多个单词。在定义final的数据不论是常量、对象引用还是数组,在主函数中都不可以改变。否则的话,会被编辑器拒绝并提示错误信息。
二、Final关键字与static关键字同时使用。 由于Java是面向对象的语言,所以在定义常量的时候还有与其它编程语言不同的地方。如一段程序代码从编辑到最后执行,即使需要经过两个过程,分别为代码的装载与对象的建立。不同的过程对于常量的影响是不同的。现在假设有如下的代码:
Private static Random rd1=new Random(); //实例化一个随机数生成对象。
Private final int int1=rd1.nestInt(10); //生成随机数并赋值给常量int1
Private static final int int2=rd1.nestInt(10); //生成随机数并赋值给常量int2
这上面的语句的大致含义是,通过Java语言提供的随机数类对象,生成随机数。并把生成的随机数赋值给常量int1与int2。细心的读者会发现,虽然同样是赋值语句,但是以上两个语句中有一个细小的差别,即在第二条语句中多了一个关键字static。关于关键字的用途,笔者在以前的文章中也有谈到过。这个是一个静态的概念。即当利用这个关键字来修饰一个变量的时候,在创建对象之前就会为这个变量在内存中创建一个存储空间。以后创建对对象如果需要用到这个静态变量,那么就会共享这一个变量的存储空间。也就是说,在创建对象的时候,如果用到这个变量,那么系统不会为其再分配一个存储空间,而只是将这个内存存储空间的地址赋值给他。如此做的好处就是可以让多个对象采用相同的初始变量。当需要改变多个对象中变量值的时候,只需要改变一次即可。从这个特性上来说,其跟常量的作用比较类似。不过其并不能够取代常量的作用。
那么以上两条语句有什么差别吗?我们首先来看 Private final int int1=rd1.nestInt(10)这条语句。虽然int1也是一个常量,但是其是在对象建立的时候初始化的。如现在需要创建两个对象,那么需要对这个变量初始化两次。而在两次对象初始化的过程中,由于生成的随机数不同,所以常量初始化的值也不同。最后导致的结果就是,虽然int1是常量,但是在不同对象中,其值有可能是不同的。可见,定义为final的常量并不是恒定不变的。因为默认情况下,定义的常量是在对象建立的时候被初始化。如果在建立常量时,直接赋一个固定的值,而不是通过其他对象或者函数来赋值,那么这个常量的值就是恒定不变的,即在多个对象中值也使相同的。但是如果在给常量赋值的时候,采用的是一些函数或者对象(如生成随机数的Random对象),那么每次建立对象时其给常量的初始化值就有可能不同。这往往是程序开发人员不愿意看到的。有时候程序开发人员希望建立再多的对象,其在多个对象中引用常量的值都是相同的。
要实现这个需求的话,有两个方法。一是在给常量赋值的时候,直接赋予一个固定的值,如abcd等等。而不是一个会根据环境变化的函数或者对象。像生成随机数的对象,每次运行时其结果都有可能不能。利用这个对象来对常量进行初始化的时候,那么结果可能每次创建对象时这个结果都有可能不同。最后这个常量只能够做到在一个对象内是恒定不变的,而无法做到在一个应用程序内是恒定不变的。另外一个方法就是将关键字static与关键字final同时使用。一个被定义为final的对象引用或者常量只能够指向唯一的一个对象,不可以将他再指向其他对象。但是,正如上面举的一个随机数的例子,对象本身的内容的值是可以改变的。为了做到一个常量在一个应用程序内真的不被更改,就需要将常量声明为staitc final的常量。这是什么意思呢?正如上面笔者所说的,当执行一个应用程序的时候,可以分为两个步骤,分别为代码装载与对象创建。为了确保在所有情况下(即创建多个对象情况下)应用程序还能够得到一个相同值的常量,那么就最好告诉编译器,在代码装载的时候就初始化常量的值。然后在后续创建对象的时候,只引用这个常量对象的地址,而不对其再进行再次初始化。就如同Private static final int int2=rd1.nestInt(10)这种形式来定义常量。如此,在后续多次创建对象后,这个常量int2的值都是相同的。因为在创建对象时,只是引用这个常量,而不会对这个常量再次进行初始化。
由于加上这个static关键字之后,相当于改变了常量的作用范围。为此程序开发人员需要了解自己的需求,然后选择是否需要使用这个关键字。在初始化常量的时候,如果采用函数(如系统当前时间)或者对象(如生成随机数的对象)来初始化常量,可以预见到在每次初始化这个常量时可能得到不同的值,就需要考虑是否要采用这个static关键字。一般情况下,如果只需要保证在对象内部采用这个常量的话,那么这个关键字就可有可无的。但是反过来,如果需要在多个对象中引用这个常量,并且需要其值相同,那么就必须要采用static这个关键字了。以确保不同对象中都只有一个常量的值。或者说,不同对象中引用的常量其实指向的是内存中的同一块区域。
二,Java 变量
Java数据类型图:
1.基本数据类型
基本数据类型,也称内置类型,是可以在栈直接分配内存的,Java保留基本数据类型最大的原因也在此:性能。关于这一点可以参考:Java为什么需要保留基本数据类型。 另外,要注意,Java是基于JVM的,所以,其所占字节固定,与机器平台无关,所有地方统一占用内存大小(除了boolean,以及byte/short/boolean数组的时候每个单元所占的内存是由各个JVM自己实现的)。 总共有四类八种基本数据类型(注1):1).整型:全部是有符号类型。1.byte:1字节(8bit),高位为符号位,其余7位为数据位,范围:-2的7次方~2的7次方-1(1111,1111~0111,1111),即-128~127(下面的计算方式相同);
注意:byte类型虽然在语义(逻辑)上是占用1字节,但实际上,JVM中是将其当做int看
的,也就是事实上是占用了32位,4字节的,所以其运算效率和int没区别,short也一样。
之所以要有byte/short类型,一是因为某些地方要明确使用这些范围类型,二是,
在byte[]数组中,JVM存储的则是真的1字节,short[]2字节。(但也有的JVM其byte[]
数组也是4字节1位)
2.short:2字节(16bit),高位为符号位,其余15位为数据位,范围:-2的15次方~2的15次方-1,即-32768~32767;
3.int:4字节(32bit),范围-2的31次方~2的31次方-1;Java默认的整型类型,即:
long l = 0xfffffffffff;//0x表示这个数是16进制数,0表示8进制。
//编译器报错,因为右边默认是int,但其超出了范围(没超出int范围的话
//编译器会隐式将int转为long),故报错(同样的错误也会出现在float)。
同样的还有:
short s = 123;//(这个123也是int类型,这里,= 操作编译器能隐式转换)
s = s + 123;//编译器报错,那是因为s+1是int类型(编译器先将s转化为int,再+1),
//这里,+ 操作编译器不能隐式转换(会提示失真,即精度可能会受损),正确的做法:
s = (short)(s + 123)//注意,不是(short)s + 123。
类型转化详见:Java 数据类型转化。4.long:8字节(64bit),范围:-2的63次方~2的63次方-1;声明大的long方法:
long l = 0xfffffffffffL;//即在后面加上L或l。
//(强制转化:long l = (long)0xfffffffffff也没用)
2).浮点型5.float:4字节(32bit),单精度,数据范围:(-2^128)~(-2^(-23-126))-(0)-(2^-149)~2^128。浮点数,通俗来说就是小数,但是,这是有精度要求的,即在这区间float可不是能表达任意小数的,而是在一定精度下,比如float有效位7~8位(包括整数位和小数位,有效小数位是6~7位,这里为什么是7~8(6~7),参考:Java中float/double取值范围与精度),即0.123456789后面的9JVM是不认识的(8能认识,整数位为0则不算是有效位,例如12.1234567后面的7也不认识,只有6位有效小数位(注意,看的是有效位,不是有效小数位,float有7~8位有效位)),即:
if(0.123456781f == 0.123456789f){//注意后面加f/F,否则就是double
System.out.println("true");
}else{
System.out.println("false");
}
//打印结果:true
//事实上,浮点数值的比较是不能直接用==判断的,这里原因就要追究到浮点数的内存结构
//浮点数比较可以用一个差值,但这种情况只是近似的比较
//如果想要精确,可以使用BigDecimal
System.out.println(Float.MIN_VALUE);//1.4E-45 = 2^-149
//这里的“最小值”意味float能表示的最小小数,实际上float最小值等于最大值取负
System.out.println(Float.MAX_VALUE);//3.4028235E38 = 2^128
6.double:8字节(64bit),双精度,范围:-2^1024~(-2^(-1022-52))-0-(2^-1074)~2^1024,Java默认的浮点类型,即若后面不加f/F,默认是double类型,即:
float f = 1.23;//编译报错,因为
float f = 1.23f;//或float f = 1.23F;
//默认是double,1.23(double)转成float,做隐式转换,但是double转成float是
//取值范围大的转成取值范围小的会损失精度,因此不能转换(详见Java数据类型转换)
//那为什么,int可以转换成byte、short,int范围更大不是?
//前面已经说过了,byte、short实际在JVM上就是int,因此编译器是不会认为会损失精度的
//但是int是不能转换成boolean,虽然boolean也是4字节(一般JVM),但在JVM认为这
//两者完全是两个东西,当然不能转换(强制也不行,你不能把猫强制转换成鸟,完全两个物种),而byte、short都是整型,同int是一个类型
3).字符型7.char:2字节(16bit),表示一个字符(可以是汉字),字符编码采用Unicode(说的更准确点,字符集(charset)采用UCS-2,编码(encoding)采用UTF-16),实际上就是一个16位的无符号整型,但是,要注意的是,因为随着发展,char所能代表的字符个数(UCS-2字符集)被限定死了,所以并不推荐使用。(更多内容,以及关于Unicode、UTF8/16参考:Unicode、UTF8以及Java char。)
char c = 3+5;//正确,char是无符号整型,但不能这样
int a1 = 3;int a2 = 5;char c0 = a1+a2;//这里需要强制转换才行
char c1 = -3;//编译错误,char不能表示负数,即使
char c2 = (char)-3;//编译正确,但无意义(乱码)
char c3 = '3';//正确,输出字符3
char c4 = "3";//编译错误,双引号,表示的是字符串
char c5 = '65';//编译错误,这里65是两个字符
4).布尔型8.boolean:逻辑上:1bit,但是实际上,boolean并没有具体规定,完全是看各个JVM实现,不过《Java虚拟机规范》给出了4个字节(同byte解释)和boolean数组一个字节的定义。
注1:(1).这种分法是一种比较流行的分法,事实上应该为两种:数值类型与布尔型。数值类型分为整型和浮点型。整型包括:byte、short、int、long、char;浮点型:float、double;布尔型boolean。之所以将char认为是整型是因为char在JVM就是以无符号整型存在的。(2).事实上Java中除去这8种以及对象类型,还有一种比较特殊的类型存在,那就是Void。java.lang.Void,是一个占位符类,不可实例化,保存着Java关键字void的Class对象。为什么说它特殊呢?明明是一个类,难道不是对象类型?那是因为void.class.isPrimitive()(这个方法是用来判断一个Class对象是否是基本类型的)返回的是true,所以Void也算是基本类型的一个了(错了),只不过它比较特殊,不能算是一种数据,只是一种象征。20160921 改:上面弄错了,把Void和void两个混为一体了,事实上,可以简单的把这两者的关系看成类似包装类和基本类型的关系,像Integer和int的关系,java.lang.Void是一个不可实例化的占位符类来保存一个引用代表了Java关键字void的Class对象:
public static final Class<Void> TYPE = Class.getPrimitiveClass("void");
而Integer也有类似的语句:
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
区别只是,Void仅仅是为void服务,即所谓的占位符类,不做他用。所以Void类只是一个普通类,而void则可以认作为如同int一样的基本类型。
2.引用数据类型
也称对象变量类型,复合数据类型,包含类、接口、数组(除了基本类型外,就是引用类型)。引用类型与基本类型最大的区别在于:
int a = 5;//这里的a是对象(严格来说不算是对象,只是个符号标识),5是数值
Integer a = 5;//这里的a是一个引用,5才是一个对象,更形象常见的是:
Object o = new Object();//o是引用(栈中),new Object()是对象(堆中)
//第二行代码中,5被自动包装成Integer对象
这里的引用有点像C/C ++中的指针,但是同指针不同的是,你不能通过改变它的值从而去改变它所指向的值。即
ClassA p = new ClassA();//C++中,这个时候是可以这样操作的:
p = p + 1;//向前移动一个单元,Java则不能
//这种操作,其实是对内存直接的操作,很显然,Java是不允许程序员做这种操作的
其实质就是,Java的引用不支持对内存直接操作,而指针则可以,所以,Java用起来更安全,但不够灵活,而指针,自由度大,但同时,要更加小心因为指针操作不当而引起的各种内存问题。在Java中,任何对象都需要通过引用才能访问到,没有引用指向的对象被视为垃圾对象,将会被回收。 引用,其实质同指针一样(可以理解为受限制的指针),存放的是一个地址,至于是实例对象的地址,还是一个指向句柄池的地址(这里可以参考:(3) Java内存结构),完全是看各个JVM的实现了。 Java中的枚举类型,都是Enum类的子类,算是类中的一种,也是引用类型。 引用类型又称为对象变量类型,是相对于基本数据类型来说的(基本数据类型不是对象),而又被称为复合数据类型,可以这样理解,引用类型的数据最终都是由基本数据类型构成的。而像接口,接口是不能实例化的,最终的实现还是由类实现的;数组在JVM中的实现也是通过类实现的,每个类型的一维数组,二维数组……都是一个类,只是这是一个特殊的类,它的对象头有别于一般对象的对象头(最主要的就是,数组对象头有对象长度)
3.变量的作用域
规定了变量所能使用的范围,*只有在作用域范围内变量才能被使用*。根据变量*声明地点的不同*,变量的作用域也不同。根据*作用域的不同*,一般将变量分为不同的类型:*类变量、局部变量、方法参数变量及异常处理参数变量*。下面对这几种变量进行详细说明。
4.变量类型
类变量
类变量也称为成员变量,声明在类中,不属于任何一个方法,作用域是整个类。
例 1:假设在一个类中声明了 3 个变量,下面编写一个测试类输出引起变量的值改变的示例代码。变量声明,实现代码如下所示:
- public class DataClass
- {
- int price=100; //定义类变量 price
- price String name; //定义类变量 name
- name int num; //定义类变量 num
- }
测试类代码如下所示:
1.publicclassTest
2. {
3.publicstaticvoidmain(String[] args)
4. {
5.DataClassdc=newDataCLass();
6.System.out.println("name="+dc.name);
7.System.out.println("num="+dc.num);
8.System.out.println("price="+dc.price);
9. }
10. }
运行结果如下:
name=null
num=0
price=100
在第一段代码中3 个成员变量,并对其中第一个变量 price 进行了初始化,而第二个 name 变量和第三个变量 num 没有进行初始化。由输出结果可以看出,第一个变量的值为显示初始化的值,第二个和第三个变量的值则为系统默认初始化的值。
局部变量
局部变量是指在方法或者方法代码块中定义的变量,其作用域是其所在的代码块。
例 2:声明两个局部变量并输出其值,实现代码如下:
1.publicclassTest2
2. {
3.publicstaticvoidmain(String[] args)
4. {
5.inta=7;
6.if(5>3)
7. {
8.ints=3; //声明一个int类型的局部变量
9.System.out.println("s="+s);
10.System.out.println("a="+a);
11. }
12.System.out.println("a="+a);
13. }
14. }
上述实例中定义了 a 和 s 两个局部变量,其中 int 类型的 a 的作用域是整个 main() 方法,而 int 类型的变量 s 的作用域是 if 语句的代码块内,运行结果如下:
s=3
a=7
a=7
方法参数变量
*作为方法参数声明*的变量的作用域是整个方法。
例 3:声明一个方法参数变量,实现代码如下:
1.publicclassTest3
2. {
3.publicstaticvoidtestFun(intn)
4. {
5.System.out.println("n="+n);
6. }
7.publicstaticvoidmain(String[] args)
8. {
9.testFun(B);
10. }
11. }
在上例中定义了一个
- testFun() 方法,该方法中包含一个 int 类型的参数变量 n,其作用域是 testFun() 方法体内。当调用方法时传递进了一个参数 3,因此其输出控制台的 n 值是 3。
异常处理参数变量
异常处理参数变量的作用域是*在异常处理块中,该变量是将异常处理参数传递给异常处理块,与方法参数变量类似*。
例 4:声明一个异常处理语句,实现代码如下:
1.publicclassTest4
2. {
3.publicstaticvoidtest()
4. {
5.try
6. {
7.System.out.println("Hello!Exception!");
8. }
9.catch(Exceptione)
10. { //异常处理块,参数为 Exception 类型
11.e.printStackTrace();
12. }
13. }
14.publicstaticvoidmain(String[] args)
15. {
16.test();
17. }
18. }
在上例中定义了异常处理语句,异常处理块 catch 的参数为 Exception 类型的变量 e,作用域是整个 catch 块。