本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,原来已经分享在我的CSDN博客,现在分享在头条,希望能帮助更多码农和想成为码农的人。版权声明:本文为CSDN博主「普通的码农」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/liyongyan1202/article/details/86631477
目录
- 介绍
- 程序从哪开始执行 - main方法
- void、return和返回语句
- 常量和变量
- public、private和类的封装性
- 类的源码文件
- 对象、构造方法、引用类型和this
- 静态属性和静态方法
- 完整的一个应用
- 总结
介绍
任何事物都有其核心,Java语言的核心就是类。任何事物也都有其本质、产生的原因、存在的原因和解决的问题,我的理解是,Java的本质或其产生原因就在于跨平台,采用的技术就是抽象出JVM(Java虚拟机)这一层。
上篇我学习到如何定义类,如何定义类的属性和方法。其实重要的是如何根据需求来抽象出合理的概念即类,这其实就可以叫建模,即为客观世界的东西建立计算机程序世界(这里是Java语言)的模型。
但还有些问题没回答,第一个就是程序执行的第一条指令在哪呢?
第二个就是HelloWorld程序里面的public是什么意思?
第三个就是我们已经定义了一个类,那怎么用这个类啊?就是怎么告诉计算机我要执行类的某个方法啊?
第四个就是定义的类有可能很多很多,总不能扎堆放在一个文件下,这样看起来很乱啊,有什么组织方式吗?
下面就针对这几个问题进行一一解答。再次给出我们的第一个程序:
public class HelloWorld {public static void main(String[] args) {System.out.println("hello world!");}}
程序从哪开始执行 - main方法
上篇提到我们的HelloWorld类只有一个行为特征,就是定义的那个名为main的方法。严格意义上说,它并不属于HelloWorld类的行为特征。它其实就是程序的执行入口,就是说Java程序的执行就是从这个main方法开始的。
方法的执行需要在某个地方调用它,那么是谁在调用这个main方法呢?前面的文章说过Java程序的执行都是在JVM进程里面的,那么应该是JVM调用了这个main方法。
那么,是不是这个main方法必须得跟上面一模一样才行呢?我们可以尝试修改一下这个main方法的写法,先把public去掉,保存HelloWorld.java,重新编译并执行:
javac HelloWorld.javajava HelloWorld
可以看到如下输出:
![ea533932fd195ecea02c75206847be31.png](https://i-blog.csdnimg.cn/blog_migrate/b0994526c90e962bbadcc70c5bcdc1d3.jpeg)
验证main方法签名-结果1
再把static去掉,保存HelloWorld.java,重新编译并执行,可以得出如下输出:
![4520e8ba4a38dc91d7d98fb381478247.png](https://i-blog.csdnimg.cn/blog_migrate/026227c6f7bd1ac9652802b6ca55cf7c.jpeg)
验证main方法签名-结果2
可以看到,这次的提示不太一样,上面是提示找不到main方法,而这次是提示main方法不是static,至于这个static是什么作用,下面我们再讨论。
void、return和返回语句
我们再把main方法的响应类型或者返回类型 void 修改为String,再重新编译并执行,可以看到编译就出现了错误,提示说是缺少返回语句。
![a3d364a709ff9b3a6d68bd3a465c666f.png](https://i-blog.csdnimg.cn/blog_migrate/5b89945d0bac9f1480562b652aa8a303.jpeg)
验证main方法签名-结果3
没错,void也是个关键字,表示一个方法不需要返回任何东西给调用此方法的地方,所以方法体里面也就不需要返回语句。而如果把main方法的返回类型 void 修改为String,就表示该方法执行结束后需要返回一个类型为String的对象(后面再讨论对象)。那程序该怎么写呢?答案就是使用return关键字。这就是返回语句了,当然语句都是以分号结尾的,比如:
public class HelloWorld {public static String main(String[] args) {//void改为StringSystem.out.println("hello world!");return "hello world!";//这就是返回语句,这里返回一个字符串常量}}
再重新编译并执行,可以看到这次又有不一样的提示:
![c3f85c2b35e84a4728785ce84ff0f661.png](https://i-blog.csdnimg.cn/blog_migrate/6778c917dc34e31583c51024bfd68ec7.jpeg)
验证main方法签名-结果4
这回提示的是main方法必须返回空类型值,也就是说void也是一种类型,专门用于方法的返回类型,这时候main方法就不能有使用return关键字的返回语句,或者返回语句中只有return和分号:
public class HelloWorld {public static void main(String[] args) {System.out.println("hello world!");return ;//方法从这里结束,什么都不返回,这个语句}}
到目前为止,可以看到Java程序的执行入口main方法必须有public和static,返回类型必须是void,后面我们可以再依次实验:
- 把main改为Main;
- 把参数类型 String[] 改为String或 int[] 之类的;
- 把参数名字args改为arguments或别的。
可以发现,只有参数名字的变化是被允许的。也就是说Java程序的执行入口main方法必须定义成以下的样子,除了参数名字可以变以外:
public static void main(String[] args) {//main方法必须定义成这样,只有参数名字可以变//这里是业务逻辑}
这是Java语法规定的,其实还可以定义成下面那样:
public static void main(String... args) {//这里是业务逻辑}
用三个点代替方括号,这里暂不讨论,我的原则是等需要用到某种东西了再讨论。
那是不是每个类都可以定义这么一个main方法呢?答案是肯定的,那程序从哪个main方法开始执行呢?当然就是执行命令里你指定的那个类里面的main方法啊。
常量和变量
上面讨论返回语句的时候我们使用了一个字符串常量。所谓常量就是那些在程序中固定不变的数据,它又分为两类:
- 一类是直接的显式的写在语句中,比如:123、3.1415956、“hello world!”、‘A’、true、false等等,这些数据都会被Java编译器赋予一个数据类型。这种常量叫做字面量或直接量。对于字面量,还有很多相关的语法,比如数字用0或0x开头表示八进制和十六进制,用l或L结尾表示是long类型,用f或F结尾表示是float类型,单个字符使用单引号并可使用转义符反斜杠表示那些不可见的字符,字符串使用双引号等等。
- 另外一类常量就是使用final关键字修饰的变量。
常量表示的数据都没法改变,它们就静静的被Java编译器安排放在内存的某个位置,程序不能修改该位置的数据。
那么,变量应该就是可以被改变的数据了啊。没错,只不过程序执行的时候数据最终是放在内存中的,要改变某个数据,实际上就是改变存放那个数据的内存的值而已。因此,本质上变量代表的是某段内存位置,只不过那段内存位置中存放的数据是可以被改变的。
给类添加属性,实际上就是变量的声明或定义。变量的声明或定义涉及内存的分配,这里暂不讨论。这里只需要知道,不管是常量还是变量,在程序执行的时候都需要为它们分配内存,它们代表的是某段内存位置,只不过常量所代表的内存中的数据是不可改变的,变量所代表的内存中的数据是可以被改变的。
public、private和类的封装性
Java程序执行入口的main方法里面有个public关键字,这个代表什么意思呢?正如开篇所说,任何事物都有其存在的原因或解决的问题。而public关键字就是为了解决类的封装性而存在的。当然,还有private、protected关键字,这里我们就先讨论public和private,protected等需要用到的时候再说。
所谓类的封装性,一个类只把该暴露给外面的才暴露给外面,不该暴露的都封装在内部,不让外部直接使用。所以,正如public和private这两个关键字的单词意思一样:
- public表示类或者类的某个属性或方法是暴露给外面直接使用的,是公有的;
- private表示类的某个属性或方法是私有的,外面是使用不了的,当然就只能是给该类本身的成员使用了。
注意,public甚至可以修饰一个类哦,而private是不可以修饰类的!
考虑类的封装性其实也属于一个类的抽象过程,就是抽象一个类的过程中你必须考虑这个类可以给所在包(后面讨论)之外的类使用吗?这个类的哪些属性或方法是公有的,哪些是私有的?其实,这里有个约定俗成的原则:
- 一般把类的属性都置为私有的,通过一些特定的公有方法来获取或修改这些私有的属性,这就是所谓的叫做getter和setter的方法了。而方法则根据需求来设置。
比如,我们考虑一下上篇中设计的Person这个类:
class Person {String name;int age;String idNumber;double money;Product buySomething(String productName, double productPrice) {//这里是买东西的逻辑}}
Person这个类可以给任何包中的类使用,应该用public修饰;其次,应该把Person类的属性都置为私有的,不能随意让外部就能访问和修改,而把buySomething方法置为公有的,因为我们要设计成的系统是外部可以叫一个人去买东西啊。经过这样一番设计,Person这个类就定义成这样:
public class Person {private String name;private int age;private String idNumber;private double money;public Product buySomething(String productName, double productPrice) {//这里是买东西的逻辑}}
再定义一个Product类:
public class Product {private String name;private double price;public String getName() { return name; }public void setName(String name) { this.name = name; }public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }}
这里Product类有名字和价格两个属性,都被置为私有的,不能直接访问,其中名字可以通过getName方法获取,通过setName方法修改;价格也是类似。至于上面的this表示什么,后续讨论对象的时候会解释。
类的源码文件
把这两个类的定义分别保存在Person.java和Product.java这两个文件中。这就是类的源码文件。
注意,Java语法规定一个源文件中只能定义零个或一个公有类(就是有public关键字修饰的类),和若干个不带public关键字的类。而且,源码文件的名字必须和公有类的类名相同(没有公有类就不需要相同)。
大家可以尝试修改源码文件,定义多个类,或没有公有类,或公有类名与文件名不一致,或多个公有类等情况,然后用javac编译。编译结果可能会报错,那么看看错误信息是什么;也可能正常通过,那么看看生成的字节码文件是什么样的。比如,我在HelloWorld.java中定义了两个如下非公有类:
class A {public static void main(String[] arguments) {System.out.println("hello world!");return ;}}class B {}
然后编译:
javac HelloWorld.java
结果是编译正常通过,而且产生了两个字节码文件A.class和B.class,然后执行:
java A
执行也正常打印出hello world!,再执行:
java B
提示在类B中找不到main方法。
这里也有一个约定俗成的规则:
- 一个源码文件通常只定义一个公有类,这样源码文件名字必须和公有类的类名相同,产生的字节码文件的名字也相同。
对象、构造方法、引用类型和this
前面说了Java程序的执行入口main方法,说了方法的返回语句,说了常量和变量,说了类的封装性和类的源码文件,但是定义好的类到底有什么用呢?或者到底该怎么用呢?这就是为什么Person类的buySomething方法的方法体一直还没有实现买东西的逻辑的原因。
之前一直强调类只是一个概念而已,是具有共同特征一类事物的抽象,它产生于人的脑子里,只是用某种语言把它固化到源码文件中而已。
然而,现实世界是一个个具体的事物组成的,这些一个个具体的事物就是一个个对象,虽然同属于一个类的对象具有共同的状态特征,但是每一个对象的状态特征的值是描述该对象的某种状态的。比如,每一个人都有名字这个状态特征,但每一个人都有自己的名字。或者每一个人都有自己的身高、体重等状态特征。
所以,还需要为我们的程序生成一个个对象,只有生成了一个个对象,才能向这些对象发送命令,让它们执行相应的方法。那么,该怎样告诉计算机说我现在要生成对象呢?答案就是使用new关键字和类的构造方法,比如生成一个产品对象:
new Product();
强调一下,这只不过是一条生成对象的指令而已,真正的生成对象是在计算机执行到这条指令的时候,本质上就是为这个对象分配内存空间,因为对象包含有自己的状态数据。就是说,对象是在程序执行过程(进程)中才存在的,源代码中是不存在的,源代码中永远只是指令而已。这也是对“程序是死的,进程是活的”的最好诠释。
可见,对象与那些基本类型的数据在本质上是相同的,都是数据而已,执行的时候会给它们分配内存用来存储,这样CPU才能访问并处理它们。只不过对象的私有数据只能由其对外暴露的方法来访问而已。
我的理解是面向对象的方法是一种以数据为中心的思维;而面向过程的方法是一种以动作为中心思维。
再来看看上面那条生成对象的语句,除了new关键字外,剩下的部分很像一个方法的调用,没错,这就是构造方法。只不过构造方法的方法名必须是类名。我们可以为自己定义的类编写构造方法,如果不编写的话,Java编译器会自动添加一个没有参数的构造方法,这个构造方法叫默认构造方法。我们可以像下面这样为Product类添加构造方法:
public class Product {private String name;private double price;/* * 这是构造方法,带有两个参数 */public Product(String name, double price) {this.name = name;this.price = price;}public String getName() { return name; }public void setName(String name) { this.name = name; }public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }}
一旦你这样定义了一个与默认构造方法不一样的构造方法,那么Java编译器就不会再为你自动添加默认构造方法了。生成对象的语句也应该改变:
new Product("产品名字