菜鸟笔记 -- Chapter 6.2.4 成员方法
6.2.4 成员方法
在Java中使用成员方法对应于类对象的行为,在有些地方也会将方法称之为函数,成员方法是定义在类中具有特定功能的一段独立小程序。方法格式如下:
修饰符 返回值类型 成员方法名 (参数类型 形式参数1,参数类型 形式参数2,......){ 执行语句; 1//int a = 2; 2//otherMethod(); return 返回值; }
通过上面看到的格式我们来进行具体的分析:
- 修饰符即权限修饰符,除了我们常见的public、protected、private和默认之外,我们有时还会看到abstract、final、和static(abstract不可以和private、final、static在一起使用,其他的final、static可以和public、private、protected及默认权限修饰符放在一起使用)。
- 返回值类型:成员方法可以有返回值和不返回任何值的选择,如果需要返回值可以在方法中使用return关键字。如果不需要返回值,那么返回值类型处用void关键字表示,可以不用return关键字,如果在方法中加return,方法的执行会被终止。成员方法的返回值可以是计算结果也可以是其他想要的数值和对象,返回值类型要与方法返回的值类型一致。
- 成员方法名:成员方法名要符合驼峰法则,并要见名知意。
- 参数类型:类型可以设置为Java中合法的数据类型
- 形式参数:形参(parameter)全称为"形式参数"是函数被调用时用于接收实参值的变量。根据实际需要可有可无。没有形参时,圆括号也不可省;多个参数之间应用逗号分隔。参数包括参数名和参数类型。由于它不是实际存在变量,所以又称虚拟变量。是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数.在调用函数时,实参将赋值给形参。因而,必须注意实参的个数,类型应与形参一一对应,并且实参必须要有确定的值。【形参中也可以是可变参数,关于可变参数,我们在方法重载中再讲解一下】形参的类型说明只有如下一种格式:
int max(int a,int b)/*形参的类型在形参表中直接说明*/{ return (a>b?a:b);}
- 实参(argument):全称为"实际参数"是在调用时传递给函数的参数. 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值。
形参和实参的区别
- 形参出现在函数定义中,在整个函数体内都可以使用, 离开该函数则不能使用。
- 实参出现在主调函数中,进入被调函数后,实参变量也不能使用。
- 形参和实参的功能是作数据传送。发生函数调用时, 主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送。
- 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。
- 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值。
- 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。
- 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。
- 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。而如果函数的参数是指针类型变量,在调用该函数的过程中,传给函数的是实参的地址,在函数体内部使用的也是实参的地址,即使用的就是实参本身。所以在函数体内部可以改变实参的值。
- 执行语句:执行语句其实指的就是方法体,是方法中的一些具体行为。在成员方法中可以调用其他成员方法和类成员变量,同时在成员方法中也可以定义一个变量,这个变量为局部变量,但是我们往往会遇到这么个情况,如果一个方法中含有与成员变量同名的局部变量,那么方法中对这个变量的访问以局部变量进行,也就是所谓的就近原则。在这里我们要说一句,类成员变量和成员方法也可以通称为类成员。
- 返回值:返回值可以是计算结果也可以是其他想要的数值和对象,但返回值一定要和返回值类型一致。当没有返回值时,返回值类型为void,同理当返回值类型为void时意味着没有返回值,此时如果在方法体中加return关键字,那么他会终止方法的运行。
方法的特点:
- 定义函数可以将功能代码进行封装,便于对该功能进行复用。
- 函数只有被调用才会被执行。
- 函数的出现提高了代码的复用性。
- 对于函数没有具体返回值的情况,返回值类型用关键字void表示(意为返回值为空),那么该函数中的return语句如果在最后一行可以省略不写。
注意:函数间是平级关系,函数中只能调用函数,不可以在函数内部定义函数(嵌套定义)。定义函数时,函数的结果应该返回给调用者,交由调用者处理。在类中除了成员方法还有两类特殊的方法,即构造方法和主方法,下面我们来认识一下。
6.2.4.1 构造方法
在类中除了成员方法以外,还存在一种特殊类型的方法,那就是构造方法。构造方法是一个与类同名的方法,对象的创建就是通过构造方法完成的。每当实例化一个对象时,类都会自动调用构造方法。构造方法的特点如下:
- 构造方法没有返回值。
- 构造方法的名称要与本类的名称相同。
- 不能被static、final、native、abstract和synchronized修饰,不能被子类继承。
注意:在定义构造方法时,构造方法没有返回值,但这与普通没有返回值的方法不同,普通没有返回值的方法使用public void methodEx()这种形式进行定义,但构造方法并不需要使用void关键字进行修饰。在构造方法中可以为成员变量赋值,这样当实例化一个本类的对象时,相应的成员变量也将被初始。
- 如果类中没有明确定义构造方法,编译器会自动创建一个不带参数的默认构造器。
- 如果在类中定义的构造方法都不是无参的构造方法,那么编译器不会为类设置一个默认的无参构造方法,当试图调用无参构造方法实例化一个对象时,编译器会报错、所以只有在类中没有定义任何构造方法时,编译器才会在类中自动创建一个不带参数的构造方法。
【本人使用了反编译软件测试了,使用了两款都没发现class文件中有构造函数,使用Oracle提供的javap反编译后,发现有如下一条语句:
所以我们实际上编译器的确自动创建了一个不带参数的默认构造器,我们该上边的函数,添加一个带参构造,然后再反编译:
public int i; public ModifierTest(int i) { super(); this.i = 7; }
我们发现此时没有了无参构造函数,所以身体力行的验证了上面.】
构造方法的定义语法格式如下:
public 类名(){ //......构造方法体 }
我们下面通过一段代码来认识一下构造函数:
package cn.yourick.constructor; public class ConstructionMethod { //成员字段 public static int a ; public static int b ; //无参构造 public ConstructionMethod() { this(3,4);//this只能放在第一行 // this.test1();死循环,内存溢出 System.out.println("无参构造!"); } //在构造方法中为成员变量赋值,实例化一个本类对象时,相应的成员变量也将被初始化,在main方法中验证 public ConstructionMethod(int a, int b) { this.a = a; this.b = b; System.out.println("有参构造方法!"); } public static void main(String[] args) { // 不创建对象,那么也不会主动运行构造函数,只会有了new的行为才会调用构造方法 ConstructionMethod constructionMethod = new ConstructionMethod(4,5); System.out.println(a+b); test1(); } public static void test1(){ //类中有有参构造,而又没有主动创建无参构造时,编译器不会主动设置无参构造, //此时创建对象不能调用无参构造,只能调用有参构造,相应的new后面跟的构造函数也应是带参的 ConstructionMethod constructionMethod = new ConstructionMethod(); System.out.println(a+b); } }
- 自定义了有参构造,那么编译器不会再为我们自动创建无参构造,我们创建对象只能使用已存的构造方法;
- 可以在构造函数中调用其它方法
- 可以在构造方法中初始化成员变量
构造函数我们先简单认识到这,后面详解面向对象三大特性继承时再详解构造函数,并介绍关键字this和super。
6.2.4.2 主方法
主方法是类的入口点,它定义了程序从何处开始;主方法提供了对程序流向的控制,Java编译器通过主方法来执行程序。主方法的语法如下:
public static void main(String[] args){
//方法体
}
在主方法的定义中可以看到主方法具有以下特性:
- 主方法是静态的,所以如要直接在主方法中调用其他方法,则该方法必须也是静态的。或者实例化后通过实例化对象调用。Main函数之所以要是静态,那是因为虚拟机执行该方法时,尚未有任何对象创建,所以要静态化.
- 主方法没有返回值。
- 主方法的形参为数组。其中args[0]~args[n]分别代表程序的第一个参数到第n个参数,可以使用args.length获取参数个数。
下面我们通过一个例子来看一下:
在项目中创建TestMain类,在主方法中编写以下代码,并在Eclipse中设置程序参数。
在Eclipse中设置程序参数的步骤如下:
(1)在Eclipse中,在包资源管理器的项目名称节点上单击鼠标右键,在弹出的快捷菜单中选择“运行”/“运行配置”命令,弹出运行配置对话框。
(2)选择“自变量”选项卡,在“程序自变量”文本框中输入相应的参数,每个参数之间回车隔开,如下所示:
package cn.yourick.constructor; public class TestMain { public static void main(String[] arg) {//定义主方法 for(int i=0;i<arg.length;i++){//根据参数个数定义for循环 System.out.println(arg[i]);//循环打印参数内容 } } }
从上面我们可以知道主方法有这么几方面的要求:
1.必须为public,否则运行时会报错,但不会编译报错
2.参数必须为String[] args,否则运行时会报找不到主类。当然args参数名称随便命名都可以。
package cn.yourick.constructor; public class TestMain { public static void main(int[] argggg) {//定义主方法 for(int i=0;i<argggg.length;i++){//根据参数个数定义for循环 System.out.println(argggg[i]);//循环打印参数内容 } } }
3.必须为static方法,否则运行时会报找不到或无法加载主类。
4.返回值必须为void,曾尝试过返回int糊弄,运行时报找不到或无法加载主类。
【主方法作为类的入口,该Java类由java虚拟机(JVM)调用,所以java类应把该方法暴露,故用public;再者,既然由JVM调用该方法,肯定不能new一个对象再由对象调用该方法,应直接由JVM调用故用static;另外,给JVM返回东西是无意义的 ,故用void;最后,括号里的参数是由JVM传给该方法的,具体可为(1)、从cmd控制台传入(2)、从开发环境IDE(Eclipse)的runconfiguration配置参数传入。
main()方法是Java应用程序的入口点,每一个Java应用都是从main()方法开始的。主方法main()的每一个参数含义如下所示。
- public:访问限制符是public,说明main()方法可以被外部调用。
- static:表示main()方法是静态方法,可以通过类名直接调用。
- void:表示main()方法不需要返回值。
- main:main是主方法的默认方法名,在执行程序时需要找到方法名为main的方法。main不是关键字,main函数只是JVM的一个特殊方法,格式是固定的除了参数名称.所以main函数也是可以重载的.
- String[] args:表示运行时参数,可在执行java命令时加入参数,格式为“java 类名 参数1 参数2 ……”。
- 主方法main()接收一个String类型的数组参数,该数组保存执行java命令时传入的参数。
- 因为main()方法是静态方法,JVM只需要加载main()方法所在的类就可以执行main()方法,不需要创建实例对象,但main()静态方法不能直接访问非静态内容。若想要主方法直接调用本类中的方法,则要定义为静态的.
6.2.4.3 方法的重载与重写
既然讲到了成员方法,那么就不得不讲方法重载了,方法重载是让类以统一的方式处理不同”数据集”的一种手段。多个同名函数同时存在,具有不同的参数个数/类型。
Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法。
重载(overloaded)和多态无关,真正和多态相关的是覆盖(inheritance)。当派生类(子类)重新定义了基类的虚拟方法后,基类根据赋给它的不同的派生类引用,动态地调用属于派生类的对应方法,这样的方法调用在编译期间是无法确定的。因此,这样的方法地址是在运行期绑定的(动态绑定)。重载只是一种语言特性,是一种语法规则,与多态无关,与面向对象也无关。重载与重写的区别就在于是否覆盖,重写一般多发生在不同的类且存在继承关系之间,而重载多是在一个类里或者一块代码段里。
方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在Class文件格式中,只要描述符不是完全一致的两个方法就可以共存。也就是说两个方法如果具有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class文件中的。
特征签名:特征签名在Java语言层面和java虚拟机层面都有定义,两者的定义并不相同。 Java语言层面的特征签名 = 方法名 + 参数类型 + 参数顺序;Java虚拟机中定义的是针对字节码的,方法的特征签名包括方法的返回值以及可能抛出的受检查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。在Java语言层面,描述符与特征签名不同;在Java虚拟机规范中,描述符与特征签名是相同的。
根据上面的解释我们来对重载进行一些练习:
package cn.yourick.overload; import judge.object.Test; public class OverloadTest { public static void main(String[] args) { OverloadTest overloadTest = new OverloadTest(); overloadTest.test(7); overloadTest.test("重载!"); overloadTest.test("重载参数1", "重载参数2"); overloadTest.test(1, "12"); overloadTest.test("1", 1); } public void test(String str){ System.out.println(str); } public void test(int i){ System.out.println(i); } public void test(String str,String str2){ System.out.println(str+"--"+str2); } public void test(String str,int i){ System.out.println(str+"--"+i); } public void test(int str,String i){ System.out.println(str+"--"+i); } //JVM中根据标识符(含返回值)来判定两个方法是否相同,但是语言层面的特征签名是以方法名和参数个数及参数类型来判定的,所以这个方法编译不过去 //可以在class文件中改变源码,已验证,但没有意义 /*public int test(int i){ return i; }*/ }
从上面我们可以明白这三个test语句都是方法的重载,有数据类型不一样的,也有参数个数不一样的.语言层面的重载取决于参数类型和参数个数[参数顺序也作为评判依据],返回值不作为评判依据.
6.2.4.4 不定长参数
上面我们知道方法参数个数不同可以作为重载的依据,那么如果存在这样一种情况呢,那就是我们知道一个参数类型,但是传入参数个数确实不确定的,此时该怎么做呢?这就使我想起了不定长参数.
不定长参数也叫可变参数,是JDK1.5新增的属性.可变参数:适用于参数个数不确定,类型确定的情况,java把可变参数当做数组处理。可变参数的定义格式如下:
返回值 方法名称(参数类型...参数名称,其它参数){
方法体;
}
在参数列表中使用”...”定义可变参数,参数列表中只能有一个可变参数,并且只能放在参数列表的最后,参数列表中除了可变参数外,也可以有其它参数,可变参数实际上是一个数组,数组长度在传递参数的时候动态确定,编译器会将可变参数作为数组格式处理,调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中一数组的形式访问可变参数。定义可变参数仍然是重载的一个延伸,下面我们来验证一下:
package cn.yourick.overload; import java.util.Arrays; public class OverloadDemo { public static void main(String[] args) { int[] ii = {1,2,3,4,5,6,8}; OverloadDemo overloadDemo = new OverloadDemo(); overloadDemo.test(); overloadDemo.test(7); System.out.println(overloadDemo.test(7,ii)); System.out.println(overloadDemo.test(ii)); overloadDemo.test("存在", 1,2,3,4,5,6); } public void test(){ System.out.println("没有参数!"); } public void test(int i){ System.out.println("一个int参数!"); } //我们使用一个可变参数来做加法,这样我们可以通过一次书写,实现未知个数整数的相加 public int test(int...ii){ int sum = 0; //可变参数是数组,所以按照数组来处理 for (int i = 0; i < ii.length; i++) { sum += ii[i]; } return sum; } //我们实现一个复合运算,首先不定长整数的相加,然后倍增 public int test(int i,int...ii){ int count = 0; for (int j = 0; j < ii.length; j++) { count += ii[j]; } return count*i; } public void test(String str,int...iii){ System.out.println(Arrays.toString(iii)+str); } }
上面的几个test()都属于重载,其中可以看到用到了3个可变参数,在这里我们要注意一个问题,那就是可变参数的传值问题,我们可以将数组传递进去,也可以直接将多个基本类型参数传递进去,但是上述代码中,我们在倒数第二和第三个方法中,发现参数类型都是int类型,所以如果我们按照整数类型传递参数会造成这两个方法特征签名(语言层面)无法识别的问题,所以只能以数组形式传递参数,这样语言层名的特征签名才清晰.我们反编译上面的代码,发现编译后的确将可变参数变成了数组:
// Decompiled by DJ v3.12.12.100 Copyright 2015 Atanas Neshkov Date: 2017/12/14 星期四 上午 2:33:43 // Home Page: http://www.neshkov.com/dj.html - Check often for new version! // Decompiler options: packimports(3) // Source File Name: OverloadDemo.java package cn.yourick.overload; import java.io.PrintStream; import java.util.Arrays; public class OverloadDemo { public OverloadDemo() { } public static void main(String args[]) { int ii[] = { 1, 2, 3, 4, 5, 6, 8 }; OverloadDemo overloadDemo = new OverloadDemo(); overloadDemo.test(); overloadDemo.test(7); System.out.println(overloadDemo.test(7, ii)); System.out.println(overloadDemo.test(ii)); overloadDemo.test("\u5B58\u5728", new int[] { 1, 2, 3, 4, 5, 6 }); } public void test() { System.out.println("\u6CA1\u6709\u53C2\u6570!"); } public void test(int i) { System.out.println("\u4E00\u4E2Aint\u53C2\u6570!"); } public transient int test(int ii[]) { int sum = 0; for(int i = 0; i < ii.length; i++) sum += ii[i]; return sum; } public transient int test(int i, int ii[]) { int count = 0; for(int j = 0; j < ii.length; j++) count += ii[j]; return count * i; } public transient void test(String str, int iii[]) { System.out.println((new StringBuilder(String.valueOf(Arrays.toString(iii)))).append(str).toString()); } }
除了重载(Overload)以外,还有重写Override,和重构,重写我们在继承讲.重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
方法的重写是继承中的概念,重写(还可以称为覆盖)就是在子类中将父类的成员方法的名称保留,重写成员方法的实现内容,更改成员方法的存储权限,或是修改成员方法的返回值类型。
重写和重载的区别:
l 重写是子类的方法覆盖父类的方法,要求方法名和参数都相同
l 重载是在同一个类中的两个或两个以上的方法,拥有相同的方法名,但是参数却不相同,方法体也不相同,最常见的重载的例子就是类的构造函数,可以参考API帮助文档看看类的构造方法
6.2.4.5 局部变量
在前面我们已经提到过局部变量,在这里我们最后进行一下详细探讨。
- 如果在成员方法中定义一个变量,那么这个变量被称为局部变量。
- l 局部变量是在方法被执行时创建,在方法执行结束时被销毁。
- l 局部变量在使用时必须进行赋值操作或被初始化,否则会出现编译错误。而成员变量就不会出现这种情况,成员变量不管是否初始化,编译器运行时会首先为成员变量赋值默认值,然后把类中对成员变量的赋值赋值给成员变量,如果类中没有对成员变量赋值,那么成员变量就是默认值。
- l 局部变量是有有效范围的,可以将局部变量的有效范围称为变量的作用域,局部变量的有效范围从该变量的声明开始到该变量的结束为止,简单说来就是在一对大括号之间.
- l 局部变量本身就决定了它的访问权限只在方法体中,所以局部变量不能有任何修饰符(除了类型修饰符和final).
前面我们提到过成员变量和局部变量名字可以一样,那么我们在开发中往往遇到方法中的嵌套问题,此时的局部变量会怎么样呢?在相互不嵌套的作用域中可以同时声明两个名称和类型相同的局部变量,但是在相互嵌套的区域中不可以这样声明,编译时编译器报错,下面通过一个例子来验证。
package cn.yourick.method; import java.io.ObjectInputStream.GetField; public class LocalVariable { private static String girlName; //声明一个成员变量 public static void main(String[] args) { LocalVariable variable = new LocalVariable(); variable.test(); variable.test(girlName+"实参可以是调用该方法的方法的局部表带,甚至是代码块中的变量,也可以是成员变量"); { String girlName = "君王不得为天子,半为当年赋洛神!"; variable.test(girlName); } } public void test(){ // String girlName = "memory1";注意如果方法中声明了局部变量,那么后面的代码块中不能再声明相同名称,作用域重复 { String girlName = "memory"; System.out.println("如果成员变量和局部变量同名,那么方法中使用成员变量应该以d对象.成员变量或类名.成员变量调用(仅针对static修饰):"+this.girlName); System.out.println("局部变量--代码块:"+girlName); } { String girlName = "memory2"; System.out.println("如果成员变量和局部变量同名,那么方法中使用成员变量应该以d对象.成员变量或类名.成员变量调用(仅针对static修饰):"+this.girlName); System.out.println("局部变量--代码块:"+girlName); } String girlName = "memory1";//放在代码块后面是可以的 System.out.println("如果成员变量和局部变量同名,那么方法中使用成员变量应该以d对象.成员变量或类名.成员变量调用(仅针对static修饰):"+this.girlName); System.out.println("局部变量--代码块:"+girlName); { // int girlName = 4;注意主要局部变量名称相同,哪怕类型不同也不行 // System.out.println("如果成员变量和局部变量同名,那么方法中使用成员变量应该以d对象.成员变量或类名.成员变量调用(仅针对static修饰):"+this.girlName); // System.out.println("局部变量--代码块:"+girlName); } } //形参&&实参&&局部变量 public void test(String str){ // String str = "西风多少恨,吹不散眉弯!";形参实际是被调用函数的局部变量,所以也应该注意变量名的作用域,在这里就会出现编译问题 String str1 = str; System.out.println(str1); } }
解读:我们要注意成员变量和局部变量以及方法中代码块中局部变量的作用域;下面画两幅分析图;
6.2.4.6 静态
6.2.4.6.1 静态概述
我们在开发中会遇到这么一个问题,那就是多个类可能需要共享一个数据,例如用来计算圆周径的π,如果我们在每个使用该数据的类中都声明,然后进行计算,这样就会造成代码的重复,以及内存的浪费(一个常量在内存中虽然占用空间很小,但是数量多了这也是一笔不小的开支),那么我们把这个数据声明到一个类中,然后通过实例调用然后计算,这样可不可以呢?答案是可以,但是这个比上一个更差劲,因为我们虽然解决了代码的重复问题,可是实例化对象却是一笔相对不小的内存开支,这对于高效代码来说绝对不可取,那么有没有办法只让这个数据在内存中存在一份,其它类调用不用再造成内存浪费呢?静态很好的帮助我们解决了这个问题.究竟什么是静态呢?什么是静态常量、静态变量、静态方法,静态内部类,什么又是静态代码块呢,静态有什么作用,为什么要声明静态呢?静态的生命周期又是什么?
在介绍静态变量、常量和方法之前我们首先要介绍static关键字,因为由static修饰的变量、常量和方法被称为静态变量、静态常量和静态方法。Java里面static一般用来修饰成员变量或函数。但有一种特殊用法是用static修饰内部类,普通类是不允许声明为静态的,只有内部类才可以。被static修饰的内部类可以直接作为一个普通类来使用,而不需实例一个外部类(见如下代码)
package cn.yourick.permissionmodifier; import judge.object.Test; public class PermissionTest extends ModifierTest{ final public static class PermissionTestDemo{ static int ii = 89; protected void test(){ System.out.println("static只能用于类只能是内部类!"); } } }
package cn.yourick.permissionmodifier; public class Demo { public static void main(String[] args) { int ii = PermissionTest.PermissionTestDemo.ii; System.out.println("静态内部类,可以直接通过类名调用内部类!"); PermissionTest.PermissionTestDemo permissionTestDemo = new PermissionTest.PermissionTestDemo(); permissionTestDemo.test(); } }
注意静态内部类的实例创建!Static修饰的是类成员,无需实例化可以通过类名进行调用,这样可以针对使用频繁的代码,减少内存分配实例对象的压力.下面我们详细讲一下static修饰的类成员,更加深入了解静态.
被声明为静态的区别于未声明静态的区别就在于一个是类成员,一个是实例成员;在这里解释一下,类成员:通过类名直接调用;实例成员:需要实例化对象,然后通过对象调用的.在这里我们就看出来静态的一大好处了,减少了因为实例化对象而造成的内存浪费.
下面我们通过一个例子来了解一下静态,并以此探讨static的一些注意事项:
package cn.yourick.statickey; public class StaticTest { /** * 声明一个静态的常量,我们声明常量时,一般会用到三个修饰符, * public是权限修饰符(当然也可以用protected和默认,但不建议private)、 * static是为了让数据能够共享,私有化就失去了意义,减小内存, * final是因为常量是不能修改的值,所以用final */ public static final double pi = 3.14; //声明一个静态成员变量 public static int a; //将private和static合在一起使用,编译器不会报错,但一般不会这样使用,我们使用静态是为了在类与类之间共享数据,private会使数据只能在当前类被使用,失去意义 private static int b; public int c = 9; int d = 23; public static void main(String[] args) { //带参构造中如果是静态参数,那么可以直接使用类名进行赋值,避免内存浪费 StaticTest test = new StaticTest(10, 11); //静态方法中如果有形参,那么注意应该传入静态参数,或者传入实例化的参数 staticMethod(a, b); staticMethod(test.c, test.d); test.staticFunction(a, b);//静态方法中调用非静态方法-->实例化调用(有形参)---->那么形参的传递也应该是静态或实例化 test.staticFunction(test.c, test.d); } //创建一个带参的构造方法 public StaticTest(int a,int b){ //静态变量,方法直接使用类名调用,不必实例化对象 StaticTest.a = a; StaticTest.b = b; } //静态方法如果有形参的话, public static void staticMethod(int a,int b){ System.out.println("最大值是:"+ (a>b?a:b)); } public void staticFunction(int a,int b){ System.out.println("两个局部变量和是:"+(a+b)); System.out.println("非静态方法中可以使用静态和非静态:"+(c+d)); } }
我们再来看一段代码:
package cn.yourick.statickey; public class StaticDemo { public static void main(String[] args) { /** *在静态方法中调用静态变量和方法可以直接通过类名.变量名(类名.方法名)来实现, *当然也可以通过对象.变量名(对象.方法名)来实现,但是不鼓励这样会造成内存浪费 */ // int b = StaticTest.b; StaticTest.staticMethod(3, 4); } }
我们在这里应该了解一下类的加载过程,这个我放在了反射中讲解,这里不说了【加载--链接--初始化--使用--卸载】,类在初始化的时候是先初始化类成员即静态,然后才会进行实例变量的初始化的.所以很多问题就迎刃而解了.我们在编译的时候是将对象的初始化转变为符号引用的,在JVM加载的时候将符号引用转换为直接引用(内存分配空间,这是在初始化static之前进行的),这是Java的简略一个流程,而静态是类成员,意味着加载完成后我们直接作用于类本身,实例变量则是需要作用于实例上的,所以我们如果想要在静态中使用数据的话,要么是静态,要么是实例化(需要一个实例的符号引用以便将来链接的时候将符号引用转变为直接引用).下面我们通过一个代码看一下:
package cn.yourick.statickey; public class Demo { int ii = 9; public Demo() { System.out.println("构造函数!"); } public static void main(String[] args) { System.out.println("静态函数!"); Demo demo = new Demo(); System.out.println(demo.ii); } }
显而易见,main函数执行到对象实例化的时候才会去实例化这个对象,然后通过对象去进行操作.(并非先初始化,然后操作静态函数).根据以上静态总结如下:
- l 静态方法中只能调用静态的成员变量和方法;
- l 方法中不能声明静态的局部变量,实际上在方法中声明局部变量,如果需要修饰符,只能用final;
- l static可以和public、protected及默认权限使用,但不能和private在一起使用(编译不会报错),没有意义,声明静态是为了让类之间公用数据,私有则不能被其他访问;静态仍然遵循修饰符的约束。
- l 静态成员的调用,使用类名.静态成员,不建议使用对象.静态成员;
- l Static可以修饰引用类型的成员变量,和其它基本数据类型一样.
- l 静态方法中不能使用this、super关键字;下面我来解惑一下为什么this和super不能使用;
package cn.yourick.statickey; import keyword.ThisTest; public class Demo extends Test{ int ii = 9; public Demo() { System.out.println("构造函数!"); } public static void main(String[] args) { System.out.println("静态函数!"); Demo demo = new Demo(); System.out.println(demo.ii); System.out.println(demo.hashCode()); demo.te(); Demo demo1 = new Demo(); System.out.println(demo1.hashCode()); demo1.te(); } @org.junit.Test public void te() { System.out.println(this.hashCode()); } }
package cn.yourick.statickey; public class Test { public Test() { System.out.println("父类构造函数!"); } public void test(){ System.out.println("父类方法!"); } }
Main函数是程序的入口,它是一个静态方法,此时调用main函数时不需要创建对象,所以main函数中更不需要调用this了.
this关键字指的是当前对象,但是this和我们new一个实例化对象还是很不一样的,new的对象,是在内存中分配空间创建一个对象,而this则指的是当前对象(这两个对象不同)【this只能存在于非静态方法中,而非静态方法的运行是需要对象调用的,this返回的就是这个对象】,static中不能用this和super的缘由也在于此,static是类成员,他被调用是通过类调用而非对象,所以this此时出现在static中会出现错误,因为此时类被加载完还没有当前对象,这也是Java的一种规范。Super调用的是当前对象的父类对象,所以也不能出现在static方法中。
4.2.4.6.2 静态的生命周期
静态为类成员,他的生命周期取决于类,java虚拟机在加载类的过程中为静态分配内存。static变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享,类被卸载时,静态变量被销毁,并释放内存空间。static变量的生命周期取决于类的生命周期.一般静态变量都是公用的、全局的,程序一启动就会在内存开辟一块空间,存放它们。静态不必实例化就能直接使用,所以静态方法里不能对非静态(为初始化)的成员变量做操作。一般静态方法操作静态成员变量或全局变量。
类初始化顺序:静态变量、静态代码块初始化、构造函数、自定义构造函数
结论:想要用static存一个变量,使得下次程序运行时还能使用上次的值是不可行的。因为静态变量生命周期虽然长(就是类的生命周期),但是当程序执行完,也就是该类的所有对象都已经被回收,或者加载类的ClassLoader已经被回收,那么该类就会从jvm的方法区卸载,即生命期终止。更进一步来说,static变量终究是存在jvm的内存中的,jvm下次重新运行时,肯定会清空里边上次运行的内容,包括方法区、常量区的内容。要实现某些变量在程序多次运行时都可以读取,那么必须要将变量存下来,即存到本地文件中。常用的数据存取格式:XML、JSON、Propertities类(类似map的键值对)等
与类变量不同,方法(静态方法与实例方法)在内存中只有一份,无论该类有多少个实例,都共用一个方法。静态方法与实例方法的不同主要有:静态方法可以直接使用,而实例方法必须在类实例化之后通过对象来调用。在外部调用静态方法时,可以使用“类名.方法名”或者“对象名.方法名”的形式。实例方法只能使用后面这种方式。静态方法只允许访问静态成员。而实例方法中可以访问静态成员和实例成员。静态方法中不能使用this。