文章目录
一、static的注意事项
- 静态方法只能访问静态变量和静态方法
- 非静态方法可以访问静态变量或者静态方法,也可以访问非静态的成员变量和非静态的成员方法
- 静态方法中是没有this关键字的
总结:
- 静态方法中没有this关键字和super关键字的
- 静态方法中,只能访问静态。
- 非静态方法可以访问所有。
二、代码角度解释
1)代码
为了让大家更简单理解,下面用的就是最简单的JavaBean
Student.java
public class Student {
String name;
int age;
static String teacherName;
public void show1() {
System.out.println(name + "," + age + "," + teacherName);
}
public static void method() {
System.out.println("静态方法");
}
}
StudentTest.java
public class StudentTest {
public static void main(String[] args) {
Student.teacherName = "阿玮老师"; // 对共享变量 teacherName 进行了赋值
Student s1 = new Student();
s1.name = "zhangsan";
s1.age = 23;
s1.show1();
System.out.println("==============");
Student s2 = new Student();
s2.name = "lisi";
s2.age = 24;
s2.show1();
}
}
首先将 Student
和 StudentTest
类都点开。
我们在观看的时候可以这样:右键点击上面测试类的选项卡,选择这里的 Split and Move Right
(切割 and 移动到右边)。
这样可以一左一右对比着看,相对来讲比较方便。
2)静态方法中没有this关键字
的解释
1、非静态方法中的 this
调用成员变量
看到这句话能不能反过来理解:普通的非静态方法中有一个隐藏的 this
。
确实是这样的,那么它在哪呢?
在我们非静态方法的形参中,它有一个隐藏的 this
(Student this
)。
要注意的是,这个 this
并不是我们调用方法手动赋值的,我们不能手动去给这个this
赋值。这个this
是在调用方法的时候,虚拟机
给这个方法赋值的 —— 谁调用当前的方法,this 就表示谁的地址值。
// this:表示当前方法调用者的地址值。
// 这个this:可以把它理解成是一个变量,类型是当前这个类的类型。由于现在是 Student 类,所以现在这个 this 就是 Student 类型的。这个变量记录的就是调用者的地址值。
public void show1(Student this) {
System.out.println(name + "," + age + "," + teacherName);
}
虚拟机是怎么给它赋值的?
在右边的测试类当中,对 show1()
方法调用了两次。
在第一次调用的时候,虚拟机它就会把它的调用者 s1
赋值给这里的 this
;
第二次调用的时候,虚拟机它就会把它的第二个调用者 s1
赋值给这里的 this
;
我们可以写代码去验证一下,在右边的测试类中创建完 s1
、s2
后分别打印出它们的地址。
在左边的 show1
方法中打印 this
的地址。要注意的是,第一次是 s1
调用的,第一次打印的应该就是 s1
的地址;第二次是 s2
调用的,第二次打印的应该就是 s2
的地址。
右键运行看下效果,程序运行完后,可以看见两个地址都是一样的。
正是因为 this
有了这样的特性,所以在这个方法里面,调用成员变量的时候,就可以区分不同的对象了。
又因为我们在调用成员变量的时候,其实每个成员变量前面都有一个隐含的 this
。
public void show1(Student this) {
System.out.println(this.name + "," + this.age + "," + teacherName);
}
这样当你的 s1
调用 show1()
的时候,此时在 show1()
方法中打印的就是 s1
的 name
和 s1
的 age
。
第二次,通过s2
调用 show1()
的时候,此时在 show1()
方法中打印的就是 s2
的 name
和 s2
的 age
。
只不过我们平时在局部位置,也就是在方法里面没有跟成员位置重名,因此这里的 this.
其实是可以省略不写的,但是我们在这里写上也没有问题。
2、非静态方法中的 this
调用其他方法
在 show1()
方法中再写一个 show2()
方法。
public void show2() {
System.out.println("show2");
}
此时我们要注意一点,在 show1
中调用其他方法,在之前我们直接写方法名就可以了
但是前面它其实也有一个隐含的 this.
,这个 this
就是拿着当前调用 show1
的那个对象,再来调用 show2
。
public void show1(Student this) {
// 调用其他方法
this.show2();
}
解释
第一次的时候,是 s1
去调用的 show1
,所以说这里的 this
就被赋值成了 s1
的地址。这个时候再来看13行
代码,既然 this
是 s1
,那么13行
这句话我能不能理解成:使用 s1
再去调用 show2
,是可以的。
第二次,它是拿 s2
去调用的 show1
,因此这个 this
就表示的是 s2
。下面的 this.show2()
方法就是表示拿着 s2
再去调用的 show2()
。
所以我们知道了,在这种普通的成员方法里面,它是含有一个隐含的 this
的。这个 this
在形参的最前面,这个东西我们不能自己赋值,如果我们强行给它赋值就会报错。
它的赋值是方法在调用的时候,由虚拟机把调用者的地址值去赋值给它的。
3、静态方法中没有 this
关键字
在静态方法中的形参位置如果加上 this
,就会报错,翻译过来就是:'com.itheima.a03staticdemo4.Student.this' 不能从静态上下文中引用
。因此在静态方法中,是没有 this
关键字的。
4、Java这样设计的原因
因为像这种非静态的东西,它往往是跟对象相关的,例如 show1()
方法里面的方法体,它打印出来的就是某个对象的 name
、某个对象的 age
,因此在这个里面它必须要有 this
。
那静态的东西呢?静态的东西一般都是共享的,共享的它跟某一个对象是没有什么太大关系的,既然你跟某一个对象没有什么关系,因此Java在设计的时候,在静态方法里面就没有 this
关键字。
有了这样一个结论之后,后面的两句话就很好理解了。
![image-20240411093712975](https://img-blog.csdnimg.cn/img_convert/556d8c4294493e82a5a8bd2f93e5e3fe.png)
3)静态方法中,只能访问静态
这句话我们返回来理解就是:静态方法中不能访问非静态的东西。非静态的东西有两个:成员变量
和 成员方法
。
我们现在就在静态方法中强行调用一下 成员变量
和 成员方法
。
此时它就报错了,翻译过来就是:不能从静态上下文引用非静态字段 "name"
报错的原因其实很简单,你在这打印 name
,你是打印哪个对象的 name
?如果需要表示是哪个对象的 name
,前面就需要有一个 this.
,但是我们刚刚才说过,静态方法里面没有 this
。因此静态方法不能调用非静态的成员变量。
同样,我们在静态方法中调用非静态成员方法 show1()
也是会报错的。
翻译过来就是:不能从静态上下文中引用非静态方法 "show1()"
报错的原因其实也很简单,如果说我们想要调用其他的非静态的成员方法,在前面也有一个隐含的 this.
,又因为静态方法中压根就没有 this
,所以它也调不了。
4)非静态方法可以访问所有
这句话也很好理解,它在调用 static
修饰的成员变量 teacherName
的时候,也相当于它的前面有一个 this.
。
而静态有两种调用方式:1、类名调用(推荐);2、对象名调用
第二种使用对象名调用,也是可以访问到共享的数据的。因此这么去调用一点问题也没有。
同样的在非静态方法 show1()
中调用非静态的 method
方法,也是必须可以的。
调用其他的方法,前面也有一个隐含的 this.
,静态的成员方法是可以使用对象名调用的。
因此这种写法也是ok的。
搞懂了从代码方面解释这三句话,那么从语法的角度也就很好理解这三句话了。
1、静态方法中没有 this
关键字
2、静态方法中,只能访问静态内容
3、非静态方法中,可以访问所有
三、内存角度解释:静态方法中,只能访问静态内容
1)前置认知
在讲解内存之前,需要大家现有一个认知,这块内存就好比是我们程序运行的内存,静态的数据跟非静态的数据加载到内存中的时机是不一样的。
静态:随着类的加载而加载。
非静态:跟对象有关系的。
例如我们刚刚写的 Student
里面的 name
,你要想那个 name
什么时候有值,是不是我们在创建学生对象的时候它才有值。那如果我们没有创建对象,在内存当中就不会有那个 name
。
所以说当静态加载到内存之后,如果说我们没有创建对象,这个时候内存中它是没有非静态的数据的。而静态的东西它是可以互相调用的,这个是没有问题的。
但是非静态的东西是跟对象有关的,只要没创建对象,非静态的数据就不会出现在内存当中。因此静态是无法调用非静态的。
有了这个认知后,我们就从内存的角度去解释这个概念。
2)从内存角度解释
1、静态方法不能调用实例变量
也就是说,在静态方法中,它只能调用其他的静态方法和静态变量,是不能调用 非静态的方法
和 非静态的成员变量
的。即静态方法不能访问非静态。
来看下面这段代码,首先在 Student
类中有一个成员方法 name
,和一个静态变量 teacherName
。
再往下有一个静态方法 method
,静态方法中调用了 name
、teacherName
。当然我们知道这种方式的调用是错的,所以 name
用红色标记了,因为在 IDEA 中,红色就表示 错误 的意思。
下面还有一个非静态的 show() 方法
,里面同样的调用了 name
、teacherName
。
在下面的测试类中,我们首先先用类名调用 teacherName
,对静态变量做了一个赋值。然后再调用静态方法 mathod()
。
接下来就来看一下右边的内存。
在刚开始的时候,main方法首先肯定还是要进栈,这个是死套路。然后执行main方法中的第一行代码:Student.teacherName = "阿玮老师"
这行代码就用到了 Student
,用到了 Student
类,就会把 Student
的字节码文件加载到方法区,在这个里面会有所有的成员变量
和成员方法
。
并在内存中创建了一个 单独存放 静态变量的空间,我们可以把这个空间叫做 静态区
。
因此当 Student
的字节码文件加载到方法区后,静态区
就出现了。
在JDK8以前,静态区是在方法区里面的。JDK8以后,就挪到了堆空间当中。
现在在静态区中有一个静态的 teacherName
,因此它就会出现在静态区当中。由于 teacherName
是 String
,属于引用数据类型,它默认初始化值就是 null
。
然后又因为,在第一行代码中,给 teacherName
做了一个赋值,此时 null
就会被 '阿玮老师'
这个字符串所替代。
这个就是第一行代码。第二行代码 Student.method()
是调用了静态方法 method()
,Student.method
就会找到方法区中的 Student
字节码文件,然后找到里面的 method
,然后把 method
加载到栈中,在方法里面它要获取两个变量:1、name;2、teacherName
。
但由于当前的方法,是用 Student
这个类名去调用的,它就会到右边的 Student
类的静态区中找 name
、teacherName
。首先 teacherName
是可以找到的没有问题,但是 name
是找不到的,因为 name
不是静态的,既然不是静态的,就不会出现在静态区中。
通过这个我们就可以得出一个结论:静态方法不能调用非静态成员变量。
这种 非静态的成员变量
我们也称作 实例变量
,这里的实例
就是对象
的意思。通过 实例变量
这个名字我们也能发现,这个变量是跟 对象
有关的,而现在在我们的代码中并没有对象,对象都没有,那怎么调用对象里面的示例变量呢?
2、静态方法不能调用非静态的成员方法
例如,在左边的 method
方法中,我添加了一段 show()
方法,show()
方法是非静态的,所以这是一种错误的调用方式,这里也把它加上了红色标记,IDEA中红色就是错误的意思。
那为什么不能调用呢?其实我们可以把它反过来理解,假设它可以调用。
要注意的是,在刚刚我们说过,像这种普通的成员方法
在调用的时候,它必须要有一个调用者
。
在方法里面打印的 name
,实际上是调用者里面的 name
。如果说我在静态方法 mehtod
里面,强行调用 show()
方法,这个时候它并没有调用者,没有调用 show()
方法的对象?是没有的。
那么既然这个对象都没有,那么下面的 name
,它去哪找?没地找。
因此:在静态方法中同样的也不能调用非静态成员方法。
到现在为止,我们就已经知道了,静态方法中不能调用非静态的原因。
所以我们在书写方法的时候需要知道这个结论:静态方法中只能调用静态的内容,不能调用非静态的。
四、内存角度解释:非静态方法中,可以访问所有
先来看一下这段代码。Student
类里面有两个成员变量 name
、age
,有一个静态变量 teacherName
。
静态方法 method
,普通的成员方法 show()
,show()
方法里打印一个 name
,再打印一个 teacherName
。
再往下,里面有一个测试类:创建一个 Student
对象,给 name
、age
分别赋值,最后再去调用一下 show()
方法,最后再来看一下右边的内存图。
在一开始的时候还是main方法先进栈,然后执行到 Student s1 = new Student();
。现在用到 Student
类了,它首先将 Student
类的字节码文件加载到方法区,在这个方法区里面会有所有的成员变量,和成员方法。
并在内存中创建了一个 单独存放 静态变量的空间,我们可以把这个空间叫做 静态区
。
因此当 Student
的字节码文件加载到方法区后,静态区
就出现了。
在JDK8以前,静态区是在方法区里面的。JDK8以后,就挪到了堆空间当中。
而现在只有一个静态变量 teacherName
,所以 teacherName
就被存放在了 静态区中,默认初始化值为 null
。
等上面这些工作做好之后,到这里才叫做加载字节码文件成功。
加载完成之后,它才开始创建对象。等号的左边就是在栈中的 main方法
中定义了一个 s1
变量,等号的右边有 new
关键字,所以这里在堆中开辟了一个空间,假设这个空间的地址值是 0x0011
,这个空间也就是我们平时所说的对象。
它里面存的是跟对象相关的,即所有的非静态的内容:name
、age
,默认初始化为 null
、0
。
最后再将 0x0011
赋值给 s1
,这个才是创建一次对象。
如果我们想通过变量去访问静态也是可以的,我们能通过对象去访问到这个类中静态区中所有的内容。
然后再往下,去给 name
赋值,它是把 "张三"
赋值给了 s1
的 name
,s1
是 0x0011
,所以就是把 "张三"
赋值给 0x0011
的 name
,原来的 null
就被覆盖成了 "张三"
。
同理 23
赋值给 s1
的 age
,那么上面的 0
就被 23
给覆盖了。
最后用 s1
调用 show()
,这个时候要注意了,show()
方法是 s1
调用的,s1
是调用者,下面在获取 name
的时候,它获取的就是 s1
的 name
。并且通过 s1
也是可以找到 teacherName
的。
因此在控制台中就会打印 张三...null
由上述的内存分析,静态的特点就是:随着类的加载而加载,字节码文件刚开始加载到内存的时候,这里的静态的 teacherName
在内存里面就已经存在了,它是优先于对象存在的。
因此在 show()
方法当中,是可以通过调用者 s1
是可以找到 静态区
中的静态变量 teacherName
的。
那么在普通的成员方法中,能不能调用静态方法呢?能不能找到这个 method()
呢?
其实也是可以的:现在 show()
方法是 s1
调用的,s1
会找到这边的对象,这个对象又是 Student
类型的,所以说它在调方法的时候,就会找到下面的方法区。方法区里面是有静态方法 method
的。
既然找到了 method()
方法,那它就可以加载到内存当中,因此非静态的方法中可以调用所有的东西。
五、总结
static翻译过来就表示:静态的意思,它是Java中的一个修饰符,可以修饰成员方法,成员变量。
其中,被 static
修饰的成员变量叫做静态变量。它的特点就是这个变量被该类所有对象都共享。
并且这个静态变量跟对象是没有关系的,随着类的加载而加载,优先于对象存在在内存当中。
在调用的时候它有两种方式:1、类名调用(推荐);2、对象名调用。
其中,被static修饰的成员方法叫做静态方法。
它是多用在测试类和工具栏当中,在JavaBean中很少会去用,因为在实际开发当中,如果你需要在 JavaBean中去写静态方法,还需要集合到后面的一些知识才能解释,因此我们只需要记住这个结论就行了。
在调用的时候它有两种调用方式:1、类名调用(推荐);2、对象名调用。
但是我们推荐,只要是静态的,不管是变量还是方法,都用类名调用,这样更好一些。
static注意事项
1、静态方法中没有 this
关键字
2、静态方法中,只能访问静态内容
3、非静态方法中,可以访问所有
这里是给有基础的同学看的,没基础的同学可以直接跳过一下内容。
有一些课程中在这里还会多讲单例设计模式,单例设计模式虽然说是用到了 static
,但是它里面的核心亮点并不是 static
,而是跟多线程相关的。只有在多线程里面才能把单例设计模式讲的非常的精彩。因此单例设计模式我们在static中先不讲,等后面学习到多线程的时候,再带着你慢慢去分析。
六、重新认识main方法
main方法我们每天都会写,现在我们已经能知道里面每个单词的意思了。
-
public
:因为main方法是被JVM调用的,访问权限需要足够的大。 -
static
:main方法是静态的,所以虚拟机在调用的的时候不需要创建对象,直接类名就可以调用了而且因为main方法是静态的,所以测试类中其他的方法也是需要用静态修饰。
-
void
:表示方法的返回值,它表示main方法被虚拟机调用的时候,不需要给虚拟机做一个数据的返回 -
main
:main是一个方法的名字,是Java规定程序主入口方法的名。它是一个通用的名字,虽然它不是关键字,但是只有main才能被虚拟机识别,如果你写成其他名字了,虚拟机是不认识的
-
String[] args
:以前用于接收键盘录入数据的,现在是没有用的。但是Java为了上下兼容,将这个参数还是保留了。
七、以前接收数据的方式
在这需要带着大家再来读一下这里的参数:String[] args
。
在参数中看到一个 []
,表示的是数组。
String
表示的是数组里面数据的类型,即数组里面只能存字符串。
args
是数组的名字。
public class Test {
public static void main(String[] args) {
// 看到数组我就有一种浑身不舒服的感觉,看到数组我就忍不住来遍历它
//在遍历数组前先打印 args.length
System.out.println(args.length); // 打印的结果长度为0,因为默认情况下,这个数组中是没有数据的
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
}
打印的结果长度为0,因为默认情况下,这个数组中是没有数据的
在低版本的JDK中,args
是用来接收键盘输入的数据,但并不是在控制台中接收的,而是在IDEA右上角中按下图点击
此时会弹出一个窗口,如下图,在 Program arguments
(程序参数) 中,我们可以把要传递给程序的参数写在这个里面,数据之间要用空格隔开。
如果不隔开,全部写在一块,它就会把这个参数作为一个整体,交给 String[] args
数组,数组的长度就是 1
。
例如我在这里传入 Hello World Java
,三个参数用空格隔开,0
索引 Hello
,1
索引 World
,2
索引 Java
。
然后应用、保存一下。
此时再来运行。此时数组的长度已经变成 3 了,刚刚的 Hello World Java
就已经传递给这个程序了。
这种方式不需要大家掌握,了解一下就可以了,因为我们现在还想要键盘接收数据的话,直接用 Scanner
就好了,而刚刚学习的是一以前的接收方式。