目录
介绍
任何事物都有其核心,Java语言的核心就是类。任何事物也都有其本质、产生的原因、存在的原因和解决的问题,我的理解是,Java的本质或其产生原因就在于跨平台,采用的技术就是抽象出JVM这一层。
上篇我学习到如何定义类,如何定义类的属性和方法。其实重要的是如何根据需求来抽象出合理的概念即类,这其实就可以叫建模,即为客观世界的东西建立计算机程序世界(这里是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.java
java HelloWorld
可以看到如下输出:
再把static去掉,保存HelloWorld.java,重新编译并执行,可以得出如下输出:
可以看到,这次的提示不太一样,上面是提示找不到main方法,而这次是提示main方法不是static,至于这个static是什么作用,下面我们再讨论。
void、return和返回语句
我们再把main方法的响应类型或者返回类型 void 修改为String,再重新编译并执行,可以看到编译就出现了错误,提示说是缺少返回语句。
没错,void也是个关键字,表示一个方法不需要返回任何东西给调用此方法的地方,所以方法体里面也就不需要返回语句。而如果把main方法的返回类型 void 修改为String,就表示该方法执行结束后需要返回一个类型为String的对象(后面再讨论对象)。那程序该怎么写呢?答案就是使用return关键字。这就是返回语句了,当然语句都是以分号结尾的,比如:
public class HelloWorld {
public static String main(String[] args) {//void改为String
System.out.println("hello world!");
return "hello world!";//这就是返回语句,这里返回一个字符串常量
}
}
再重新编译并执行,可以看到这次又有不一样的提示:
这回提示的是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编译器赋予一个数据类型。这种常量叫做字面量或直接量。对于字面量,还有很多相关的语法,下表列出了一些:
举例 | 数据类型 | 描述 |
---|---|---|
8、+8、-8、 0b11、017、0x1a | int | 可以表示正数、负数、可以用二进制、八进制、十六进制表示 |
8l、-8L | long | 后面加上字母l或L,就变成long类型了 |
3f、-3.14F、1.2e5f、.5F | float | 以f或F结尾,可以用科学计数法表示,整数部分为0则可省略 |
3d、-3.14、1.2e-5、-.5D | double | 以d或D结尾,当然小数的话可不加 |
true、false | boolean | 布尔型的字面量只有这两个值,当然这两个单词就成为关键字了 |
‘a’、‘中’、’\n’、’\u0000’ | char | 字符型字面量必须用单引号,有些字符是特殊的,故采用反斜杠来转换意义,反斜杠就是转义符,比如’\n’就表示换行符。其实字符在计算机内部还是用一个二进制数字来表示的,因此也可以为每个字符映射一个数字,这就是字符的编码,’\u0000’中的u就表示Unicode码 |
“abc”、“你好” | String | 字符串类型字面量必须用双引号 |
null | 引用类型 | 它可以赋值给任何一个引用类型的变量,这样该变量就不指向任何对象 |
- 另外一类常量就是使用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("产品名字", 100.0);//传入相应的参数
任何方法的调用中,传入的参数就叫实参,而方法签名中的参数就叫形参。这两个叫法也很形象,实参就是参数的实际值,形参就是参数在调用前在形式上代表实际值。
可以看到构造方法正如其名字所示,是专门用来构造一个对象的方法,执行这个方法的时候就会在内存中构造一个对象,同时初始化这个对象。构造方法的方法体也遵循尽量简单的原则,如果必须很复杂,那么你就得考虑这个类是否合理了。
很容易发现构造函数的另外一个特点就是没有返回值,这里是真的没有返回值,即连返回类型都没有写,为什么呢?因为构造函数返回的必须是该类的一个对象啊,就是返回类型就是该类对象的引用啊,这就是引用类型。这跟返回类型是void的完全是两码事哦。
既然返回的该类对象的一个引用,那么就可以定义一个该类的引用类型的变量来指向生成的对象:
Product p1 = new Product("产品1", 100.0);//p1这个变量指向了一个叫产品1的Product对象
Product p2 = new Product("产品2", 200.0);//p2这个变量指向了一个叫产品2的Product对象
Product p = p1;//p也指向了p1所指向的Product对象
这下明白引用类型的含义了吧,其实它的含义就表示该变量是一个对象的引用而已,但是真正的对象是放在内存的某个位置的。可以简单理解为当一个引用类型的变量指向某个对象时,该变量的值就是该对象所在内存的位置而已。
修改一个变量的值可以使用赋值操作符,就是上面的等号啦,比如为一个整型变量进行初始化赋值并修改:
int i = 100;//在为变量i分配内存的同时,把内存的值初始化为100
i = 200;//把变量i的内存修改为200
有了指向某对象的变量,那么我们就可以通过该变量来操作该对象,Java语法是通过一个点操作符,比如我们要修改产品1的价格:
p1.setPrice(101.5);
上面的语句中,p1是要操作的对象,也可以理解为要接收某个指令(记住,方法其实就是一个粗的指令)的对象,setPrice就是指令了,后面是圆括号括起来的实参。这个语句可以理解为给对象p1发一个修改价格的指令,将价格修改为101.5。这就是Java里面的方法调用。
现在可以来看看构造方法和其他方法里面的this这个关键字的含义了。很明显,this表示的就是正在被构造或者被操作的那个对象。因为类只是概念,只是同一类对象的模板,所以必须要有一个方法来让Java编译器知道在类的方法里面如何访问一个对象的数据。
因此,执行上面的语句时,可以理解为Java编译器会把setPrice方法内的this修改为p1。
静态属性和静态方法
前面提到main方法是程序执行的入口,本质上并不属于对象的行为特征,但确表示该类的一个行为特征,就是说该行为特征不属于某个对象的,不需要给某个对象发送该指令来执行该行为。这样的行为特征在Java里面就需要用static关键字来修饰。
同理,有一些状态特征也是属于类的,而不是属于对象的,比如可以为Product类添加一个出货量的属性,用来统计一共卖出去多少个产品:
public class Product {
public static int numOfSold;//卖出去产品数量
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; }
}
在这个例子里,出货量是统计所有产品一共卖出去到少个,并不需要是每个产品对象有自己的值,因此,把它设计为产品类的属性。当然,你也可以将它拿出来放到一个专门设计的统计类中,比如ProductStatistics。这就涉及到怎么去抽象类设计类更合理的问题了。
用static修饰的属性和方法就是静态属性和静态方法。静态属性和静态方法都属于类,可以直接使用类名加点操作符访问:
类名.静态属性名
类名.静态方法名(实参列表)
这里还有一个Java语法,静态方法的方法体是只能访问到该类的静态属性和静态方法,而不能访问属于对象的属性和方法。为什么呢?原因也很简单,我们可以用反证法证明:如果能访问属于对象的属性和方法,那么像上面直接使用类名加点操作符调用静态方法时,由于不明确是给哪个对象发指令,所以静态方法里面的this就不知道指向的是哪个对象啊。
完整的一个应用
到目前为止,我们已经把我们的第一个程序分析完了,不,还差一点,就是main方法里面的这个语句:
System.out.println("hello world!");
我们可以尝试根据已有的知识推断一下,很明显,这是一个方法调用,根据方法名字推断其是打印出字符串"hello world!",这个打印方法是发给out这个对象的,而out这个对象又是属于System这个类的一个静态属性。
另外,我们还设计了自己的两个类,现在可以把它们的逻辑实现完整了,首先是产品类,保存在Product.java这个源码文件中:
public class Product {
public static int numOfSold;//卖出去产品数量
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; }
}
然后是人类,保存在Person.java这个源码文件中:
public class Person {
private String name;
private int age;
private String idNumber;
private double money;
public Person(String name) {//这里只初始化了名字这个属性
this.name = name;
}
public Product buySomething(String productName, double productPrice) {
//这里是买东西的逻辑
Product p = new Product(productName, productPrice);
Product.numOfSold++;
return p;
}
}
这里使用了自增操作符"++",关于操作符和程序的流程控制相关的知识,还是使用“用到才介绍”的原则。自增操作符的含义是,该变量的值加1。
最后是使用这两个类的用户类以及程序执行的入口类,我们还是使用HelloWorld这个类,保存在HelloWorld.java这个源码文件:
class HelloWorld {
public static void main(String[] arguments) {
System.out.println("hello world!");
Person zhangsan = new Person("张三");//生成张三这个人
Product bread = zhangsan.buySomething("面包", 5.00);//叫张三去买面包
Product water = zhangsan.buySomething("水", 5.00);//叫张三去买水
Person lisi = new Person("李四");//生成李四这个人
Product apple = lisi.buySomething("苹果", 3.00);//叫李四去买苹果
Product orange = lisi.buySomething("橙子", 3.00);//叫李四去买橙子
System.out.println(Product.numOfSold);//打印一共卖了多少个产品
return ;
}
}
源代码编辑好之后,进行编译并执行:
可以看到结果显示一共卖出去4个产品。
这只是一个简单的应用,实际上并没有什么实际用处,只是用来演示而已。剩下的工作就是我们需要根据现实的需求去抽象和设计一个一个类,将这些类组装起来形成一个系统,尽情的发挥我们的想象力吧。
总结
其实,我认为到目前为止Java的核心已经讲解完毕,剩下的Java语法都是围绕类、类的属性和方法。重要的是如何抽象和设计出合理的类,这就得靠实践不断的积累经验和总结经验,不断的靠阅读源码,不断的靠借用别人的经验了。
再次强调一下,任何事物都有其核心、本质、产生的原因、存在的原因和解决的问题,其实就是哲学里的矛盾论,透过现象看本质而已。我们设计出的系统也要让每一句代码都有其存在的必要性,要消除重复。比如,Java语法里面的所有关键字都是有其存在的理由的,多想想这些关键字是为了解决什么问题的会有助于理解Java,甚至类的设计思维。
下面是本篇的一些要点:
- 程序的执行入口是main方法,从哪个main方法执行由你来决定;
- 返回类型不是void的方法,必须有一个返回语句,一旦执行完返回语句该方法的执行就结束了;
- 不管是常量还是变量,它们代表的都是数据而已,而程序执行的时候数据是必须存放在内存中的,所以本质上它们代表的都是一段内存而已;
- 设计类要考虑类的封装性,类的封装性通过public、private等关键字来实现;
- 通常一个源码文件只保存一个公有类的代码;
- 类只是一个概念,一个模板,使用类就必须为这个类生成对象。生成对象的指令在Java中是使用new关键字和构造方法来实现的;
- 对象的本质也是数据,执行生成对象的指令就是为该对象分配内存;生成的对象需要赋值给该类的引用类型变量才能使用;
- 引用类型的变量的值可以理解为指向该对象的内存位置;
- 属于类的属性和方法就是静态属性和静态方法,它们的访问可以直接使用类名加点操作符。为什么一定需要静态属性和静态方法呢?不要行不行?可以思考一下。