JAVA基本语法+面向对象
目录
Java基本语法
类和对象
Java程序的基本组成单位是类,一个类的内部主要有三种东西:属性(field,即字段),方法,和构造方法。而对象,是类的实例。对象具有类的全部属性和方法。比如人类,有年龄和性别,能睡觉,会吃饭,而某个具体的人,也有特定的年龄和性别,也可以吃饭睡觉。
类的构造方法
构造方法是类在构造对象时执行的过程,是对所创建的对象实体的初始化,Java的构造方法其实就是C++中的构造函数,只不过C++的构造函数可以使用初始化列表,而Java没有初始化列表。下面是一个定义类的例子:
class Example{ int x ; String s ; int add3(int a){ return a + 3 ; } Example(){ x = 10 ; s = null ; } //Example类的构造方法}当一个类没有定义构造方法时,系统会自动生成一个默认的构造方法,默认的构造方法没有方法体,类内的属性所得到的初始值是默认值,如 int默认值是0,boolean默认值是false。
对象的实现
创建对象的实现,需要通过new运算符+构造方法来实现。如:
Example a = new Example();其中变量a是引用变量,new Example()创建了一个对象实体,引用变量a指向了这个实体,关于这些的具体解释,我们放到后面的引用数据类型中再进行详细说明。 通过new还可以创建匿名对象,匿名对象是没有 指向它的变量 的对象,相当于没有名字。
输入输出
输出
Java的输出比输入简便,主要原理就是调用标准的输出流(System.out)来进行输出,让我们回到梦开始的地方:
public class Hello{ public static void main(String[] args){ System.out.println("Hello world!"); }}上面的println是print line的缩写,它在输出内容后会自动换行,System.out的成员中还有print、printf等,print在输出内容后不会自动换行(这一点与python不同),printf是print format,是指定格式的输出,用法与C/C++的printf类似。
输入
Java的输入需要引入一个Scanner类,并通过Scanner类创建对象并且于标准输入流(System.in)绑定,通过这个对象实现输入操作,下面是一个简单的例子:
import java.util.Scanner;public class test{ public static void main(String[] args){ int x ; Scanner reader = new Scanner(System.in); x = reader.nextInt(); System.out.println(x); }}其中对于Scanner reader = new Scanner(System.in);这行,我们也放到后文的“引用数据类型”中再作详细解释。
数据类型
简单的了解输入输出,接下来才真正的开始走进爪洼岛的殿堂。程序不可能不和数据打道。Java中的数据类型主要有两大类:基本数据类型 和 引用数据类型。众所周知,Java是在C和C++基础上开发的,其中和C/C++不同的一点就是Java中去除了指针,这使得程序更安全,从这里就可以看出来,Java是很讲究安全性的,Java的编译器就像一位严格的检察官,排查一切包括内存泄漏、数组越界等存在风险的可能。不过,就拿数组举例,数组的底层就是连续的内存,这不得不用到地址,这貌似避不开指针,那Java是如何处理这类问题的呢?预知后事如何,请读 下文。
基本数据类型
Java的基本数据类型有:布尔型(boolean)、字节型(byte)、字符型(char)、短整型(short)、整型(int)、长整型(long)、单精度浮点型(float)、双精度浮点型(double)。(没有字符串型哦,或者说,字符串类型不是基本数据类型),与C/C++不同,Java中没有long long和usigned(无符号)类型。下面是这些基本数据类型的大致介绍。
boolean:布尔型,常量只有两个值:true和false。(注意:true和false是两个值,不是关键字)
byte:字节型,顾名思义,是占一个字节的整型,数据表示范围是-128到127的整型数据。Java中不存在byte型常量的表示方法,但可以把1个字节范围内的整型赋值给byte型变量。
char:字符型,Java中的字符使用Unicode编码,占两个字节,这点与C/C++的char类型不同。所以在之后的Java程序中你可能会见到类似这样的命名:
int 年龄 = 18 ;char 爪洼 = '哇' ;注:Java代码中的单引号、分号和等号等符号仍然要用英文。
short:短整型,是比整型短,比字节型长的一种数据类型,通常占2字节。Java中不存在short型常量的表示方法,但是可以把2个字节范围内的整型赋值给short型变量。
int:整型,这个就是众所周知的整型,通常占4字节。整型常量就是整数,例如32.
long:长整型,比整型长的一种数据类型,通常占8字节。长整型常量是整数+后缀L,例如:32L。(注意是大写L,在IDEA中用小写l的话会提示改成大写,而且小写l容易与数字1混淆)。
float:浮点型,是用来存放小数的数据类型,在《Java2实用教程(第五版)》一书中介绍的float可以保留8位有效数字,通常占4字节。float类型常量是小数+后缀f或F,例如:3.14f,5.20F。
double:双精度浮点型,精度比float高,上书中介绍的double可以保留16位有效数字,通常占8字节。double类型常量就是我们平时所见的小数,如:3.14,5.20。
小结:Java中的byte和short没有对应的常量表示,其它不加后缀的数字是int型或double型的常量,字符类型是2字节。
在C/C++中,在涉及到数据间的赋值和运算时,经常涉及到类型转换的问题,Java中也是如此,既可以隐式转换(自动转换),也可以显式转换(手动转换)。C/C++中可以随意地隐式转换,无非就是精度损失的问题,但是在Java中,那个严格的检察官可不允许这样。
之前在看书或者实践过程中,被Java的数据转换玩的团团转,杂乱无章的转换规则我难以接受,所以后来找了一个规律,这个规律应该是没有问题的,如果发现有错还请及时联系小编更正。
先看个图(作图水平有点次,那个小方框里的X代表“错号”,表示“路径不通”,不知道我画这幅图的软件怎么在曲线上打叉 ):

在 没有强制类型转换(显式转换)的情况下:
- 当变量赋值给变量时(即赋值号右边是变量时):如果上图中的右边赋值给左边,则报错;左边赋值给右边,是可以的。
- 当常量赋值给变量时(即赋值号右边是常量时):在分割线往右(包括long类型 常量 赋值给int 变量),如果上图中的右边类型的 常量 赋值给左边类型的 变量,则报错;如果左边类型的 常量 赋值给右边类型的 变量,是可以的。在分割线往左,右边类型 常量 可以赋值给左边类型 变量(包括int常量赋值给byte变量也是可以的)。
如果有显示转换:
只要赋值号两端类型一致,则没有问题,如果类型不一致,则仍然是上述规律。
下面是一些不同类型赋值的例子,第8、10、12、15行红色波浪线处是错误的赋值,编译器会报错

有没有发现,上面所说的类型里面没有boolean型,它貌似被孤立了一样,实际上,boolean与其他任何类型都不能隐式转换,直接赋值也会报错,不过boolean强制转换(显式转换)成int时,true会变成1,false会变成0。
当遇到赋值时编译器报错的情况,只要不担心精度损失的问题,就可以直接强制转换,毕竟强制类型转换的主动权还是在自己手里的,就算是再严格的检察官也不敢说你错。检察官:君让臣转,臣不得不转。
引用数据类型
上文 说到,Java中去除了指针,那么数组等需要用到地址的东西,它怎么实现呢?这个问题,被Java的引用数据类型巧妙化解。众所周知,Java和C++其中一个对于数组的区别就是:Java中数组下标越界会报错,但是C/C++不会。Java的引用变量,类似于C/C++的指针变量。引用数据类型,就类似C/C++的指针类型,不过Java的引用和C/C++的地址不完全一样,Java的引用是地址的重要信息,并非地址本身。在C++中也有引用,但是C++的引用和Java中的不同。我们先区别一下两者不不同:
C++的引用与其绑定的对象共用同一块内存,对其中任何一者操作,都会影响到二者的内容。
Java的引用与其绑定的对象 不共用 内存,Java的引用变量里面存放着与其绑定的对象的地址的相关信息,它们是两块内存,通过引用访问对象其实就是一个寻址过程,这个过程与C/C++的指针类似。
Java的引用类型包括:字符串、数组、类、接口等。
以数组举例,众所周知,Java的数组定义是不允许直接指定长度的,这一点与C/C++不同,例如在Java中:
int a[] ; //合法int[] a ; //推荐使用这种方式定义数组int a[10] ; //报错int a[10]并不是创建引用变量也没有申请内存,Java中没有这样的语句,当有int a[10]这条语句时,Java编译器:这是个什么玩意?
既然不能直接指定长度,那我们想要一个长度为10的数组该怎么办呢?
对于身为引用数据类型的数组,其实我们的int[] a;这行代码,是定义了一个int[]类型的变量a,其中int[]是一种引用类型(也就是整型数组类型),而a就是int[]型的变量,注意,这里的数组名a是 变量,这点与C/C++的数组也不同。对于指定长度的问题,我们先看另一个引用类型:
还记得本文开头介绍输入时Scanner reader = new Scanner(System.in);这行吗,这个是一个通过类来创建对象的过程,其中的Scanner是类名(注意类也是引用类型),而reader是一个引用类型的变量,new Scanner()是在内存中创建一个Scanner对象并计算出其地址信息的语句,至于括号里的System.in这个参数,这里会涉及到类的构造方法(C++中称为“构造函数”,其实两者是一个东西),我们先不谈这个参数的问题。Scanner()是Scanner类的一个构造方法,new运算符最后计算出的是Scanner类的成员的引用值(即和地址重要信息相关的值),系统看到new Scanner()这条指令时会做下面几件事:
为Scanner类的成员分配内存,然后Scanner类中执行构造方法中的指令(可以理解成对成员进行初始化),这个过程是创建对象的过程,这件事是Scanner()的作用;
计算出Scanner成员的地址的重要信息,最终的计算结果通常是一个十六进制数,通过这些重要信息可以找到这些成员。
所以,Scanner reader = new Scanner(System.in);这条语句就是计算出了Scanner成员的地址信息,然后把这个信息赋给reader变量。换言之,reader这个引用类型变量里存着创建好的Scanner类的 对象 的地址信息。这个过程也称为reader变量和创建好的对象绑定。通过reader变量,可以调用相应对象的各种方法。这里注意区分:reader是一个变量(有时被称为对象变量,可以说是对象的名字),它不是对象实体,通过Scanner()语句在内存中创建出来的那个东西才是对象实体,通过reader可以访问到那个对象。
了解过new语句后,我们再回到指定长度,我们通常见到的创建数组的方法是:
int[] a = new int[10] ;这样的语句,这条语句就是在内存中申请10个整型长度的内存,然后计算出这些内存地址的信息,再赋值给引用变量a,这就是定义数组并指定长度的正确操作。
小结:引用是地址相关信息,并非地址本身,引用变量存放着这些地址信息。
流程控制
Java的流程控制和C/C++类似,有分支,有循环等。分支有if语句和switch语句,循环有while,for,do…while语句。
分支
分支的if和C/C++类似,可以嵌套,也可以if…else等,不过与C/C++不同的是,Java中的条件判断语句最终的结果只可以是boolean类型,没有 C/C++中0是false,非0是true的说法。
分支还有一种语句是switch,它的用法和C/C++基本相同,不过在较新版本的Java中,switch还的case还支持->,下面是一个简单的例子:
int x = 3 ;switch(x){ case 1 -> System.out.println("is 1"); case 2 -> System.out.println("is 2"); default -> System.out.println("oh");}可以发现,通过->,可以省去写case中的break语句。
循环
Java的循环与C/C++基本相同,不过还是有条件判断语句的差别。我们也可以通过break,continue语句控制循环的执行,控制效果和C/C++相同,这里不再赘述。
面向对象
众所周知,面向对象有三大特性:封装,继承,多态。
封装
封装是指对类的某些信息隐藏在类的内部,防止外部成员访问和篡改,更加安全一点,不仅如此,封装可以使得外部程序只能通过某些开放的接口进行访问,这样即使类内部有了某些变化,只要接口不变,外面程序访问的代码就不需要修改,比较便于维护。
封装通常是通过private,protected等关键字来实现的,下面是一个简单地体现封装的例子:
class Circle{ private double r ; private double pi = 3.14 ; void setR(double x){ this.r = x ; } double getR(){ return this.r ; } double ComputeArea(){ return pi * r * r ; }}对于这些private等私有等级:
private:只能在本类内访问
protected:只能被同一个包的类或其他包的子类访问
public:可以被所有类访问
继承
继承,顾名思义,就是继承。 就是指子类拥有父类所有的非私有成员(private修饰的成员在子类中不能访问哦)。子类和父类是什么呢?面向对象里把一些实例A的共有属性抽象成了一个类A,但是还有一些实例B,既有实例A的属性,又有一些新的属性,此时实例B所属的类B就可以看作是类A的衍生,此时类B就是类A的子类。
一般的父子类
继承在Java中用extends来表示,举个栗子:
public class test{ public static void main(String[] args){ Student zhangsan = new Student() ; }}
class Person{ String name ; int age ;}
class Student extends Person{ int grade ;}这里的Person拥有姓名和年龄两个属性,而Student既有姓名和年龄,又有成绩,那么这时我们可以把Student看作时Person的子类,Student的姓名和年龄这两个属性是从它的父类Person中继承下来的。此时zhangsan所指向的Student的对象就含有name,age,grade三个属性。
我们定义Person类时,没用extends。Java中没用extends的类,编译器会自动加上extends Object。也就是说,除了Object类外的所有类都会有一个父类,没用指定父类的类默认是Object的子类,可以把Object看作是万类之鼻祖。上面我们定义的Person类其实就是:
class Person extends Object{ //......}密封类
接下来再想一种情况,有些类它并不想让所有类都可以继承它的属性,就像皇帝并不想当所有人的爸爸,不想让所有人都可以去继承他的皇位或财产,这时这个 爸爸 父类就可以指定谁可以继承它的东西,Java中类似这样的类我们用sealed修饰,并且通过permits来指定谁可以继承它的东西。语句类似这样
sealed class A permits B,C,D...表示:密封类A允许B,C,D…继承它的属性和方法。
不过,在定义B,C,D…这些类的时候,因为它们的父类A只指定了它们可以继承,如果我们这样:
sealed class A permits B{ //......}class B extends A{ //...... (此时B含有A的属性和B的新属性)}class M extends B{ //...... (此时M含有A和B的属性以及M的新属性)}这会喜提一个报错,因为对于密封类的继承,要求子类明确这种密封关系,也就是说,子类要决定是否可以继续继承,谁可以继承,还是都可以继承。如果不可以继续继承(即断子绝孙),则用final修饰子类,如果仍然保持密封,则和父类相似,用sealed修饰并且指定继承名单,如果要解除密封,则用non-sealed修饰,正确的继承如下:
sealed class A permits B,C,D{ //......}final class B extends A{ //......}sealed class C extends A permits E,F,G{ //......}non-sealed class D extends A{ //......}当然如果类A不想让任何类继承它的东西,也可以直接用final修饰类A:
final class A{ //.......}还需注意一点,当类A指定了继承子类B,C,但是我们只定义了类C而没有定义类C,这时编译器会告诉我们:无法解析符号“C”,这是因为类A明确指定了它的子类B和C,但是编译器在当前的编译单元找不到类C。就好像皇帝指定了张三当太子,但是这个世界上没有张三这个人一样,编译器会懵住:C是个啥玩意,是你儿子嘛,然后报错。
子类构造方法
在继承过程中,父类有自己的构造方法,子类并不会继承父类的构造方法,子类构造方法是编译器自己生成的,并非继承父类的,想要调用父类的构造方法,需要使用super关键字,通过super关键字可以调用父类(即超类)的属性、方法和构造方法。举个栗子:
class father{ int fx ; String fname ; father(){ fx = 1 ; fname = null ; } //无参数的构造方法 father(int x , String s){ fx = x ; fname = s ; } //有参数的构造方法 void speakOI(){ System.out.println("oi!"); }}class son extends father{ double sy ; son(int x , String s , double y){ super(x , s); //调用父类的构造方法 sy = y ; } void valueToX(int x){ super.fx = x ; //调用父类属性,等同于fx = x ; } void speak(){ super.speakOI(); //调用父类方法 }}其中调用父类的属性和方法时可以直接调用,也可以通过super关键字调用,但是父类的构造方法必须使用super关键字调用,而且参数要对应。上面的例子是调用了父类有参数的构造方法,对于son类的构造方法,我们还可以这样:
son(int x , String s , double y){ super(); //调用父类的无参数的构造方法 sy = y ;}//此时参数列表里的x和s没用或这样:
son(double y){ super(); //调用父类的无参数的构造方法 sy = y ;}在父类和子类都没用定义构造方法的情况下,如果我们这么写:
class A{ int x ;}class B extends A{ double y ;}对于类B,编译器会自动生成一个类B的构造方法,这个构造方法通常是这样的:
B(){ super();}其实,对于每个类,在这个类的构造方法第一行编译器都会自动加上super();语句(在父类含有无参数的构造方法的情况下,当父类没有定义任何构造方法时,系统会自动生成一个构造方法,这个也是无参数的)。那么如果父类不含有无参数的构造方法呢,这时就需要手写构造方法,否则会喜提一个报错,下面是一个父类没有无参数的构造方法的例子:
class A{ int xA ; double yA ; A(int x , double y){ //这里有一个默认的super(); xA = x ; yA = y ; }//类A的构造方法,此时不会自动生成类A的默认构造方法}class B extends A{ int xB ; B(int x , double y , int xx){ super(x , y); //写super();或者不写这一行都会报错 xB = xx ; }}这时类B的构造方法就必须通过super(参数)调用类A的构造方法。那为什么类A的构造方法也有一个默认的super();呢,这个super();调用的是谁的构造方法呢?答案就是上面说到的Object类,所有没有说明父类的类都被默认看作Object类的子类。
覆写
继承中还有一个特点就是覆写,指的是子类可以对父类的某个方法的方法体做更改,但是不同于重载,覆写只能修改函数体,不可以修改参数列表。下面是一个例子:
class A{ int returnANumber(){ return 3 ; }}class B extends A{ int returnANumber(){ int i ; for( i = 1 ; i <= 3 ; i ++ ){ if( i == 2 ) break ; } return i ; }}类B中的returnANumber方法就是对父类该方法的覆写。我们只是修改了函数体,没用修改参数列表和返回值类型(修改参数列表属于重载),如果将类B中returnANumber方法的返回值类型改成double则会报错。
关于这些继承,我们举个栗子,还是那个人类,还是那个学生类,但是学生又有好几种,比如小学生,中学生,大学生等。大学生一般会有科研、实践等。下面是一个简单的例子:
class Person{ int age ; String name ; void speak(){ System.out.println("I'm a person"); } Person(){ super(); //如果不写,系统会自动添加 } //无参数的构造方法 Person(int age , String name){ this.age = age ; this.name = name ; } //有参数的构造方法}sealed class Student extends Person permits undergraduation{ int grade ; void speak(){ System.out.println("I'm a student"); } //覆写 Student(){ super(); grade = 100 ; } //无参数的构造方法 Student(int age , String name , int grade){ super(age , name); this.grade = grade ; } //有参数的构造方法}final class undergraduation extends Student{ boolean research ; //科研 boolean practice ; //实践 void speak(){ System.out.println("I'm a big student!"); System.out.println("power!"); } //覆写 void speak(String s){ System.out.println(s); } //重载 undergraduation(){ super(); research = true ; practice = true ; } undergraduation(int age , String name , int grade , boolean x , boolean y){ super(age,name,grade); research = x ; practice = y ; }}如果在Person类中不定义无参数的构造方法,那么后面的super();就会报错,因为此时Person已经有了一个构造方法,系统不会再自动生成默认的构造方法。
多态
多态是指一个方法在被调用时,其执行结果由运行时期的实际类型决定。定义听起来很懵,我们先来看个东西:
public class test{ public static void main(String[] args){ Person a = new Student(); a.speak(); }}class Person{ int age ; void speak(){ System.out.println("I'm a person"); }}class Student extends Person{ void speak(){ System.out.println("I'm a student"); }}我们定义一个Person类的对象变量,却创建了一个Student类的对象实体,这时变量a究竟是指向的Person还是Student呢?以上程序输出I’m a student,这告诉我们,变量a是指向Student对象的变量。也就是说,父类类型的对象变量可以指向子类的实体。这一点是多态的一个基础。
下面是一个多态的实例:
public class test{ public static void main(String[] args){ Father[] a = new Father[]{ new Father() , new Son1() , new Son2() } ; for( Father i : a ){ i.speak(); } }}class Father{ void speak(){ System.out.println("I'm father"); }}class Son1 extends Father{ void speak(){ System.out.println("I'm son1"); }}class Son2 extends Father{ void speak(){ System.out.println("I'm son2"); }}上面程序的输出结果是:
I'm fatherI'm son1I'm son2同样的一个循环内,我们利用父类类型的变量可以指向子类实体这一特点,结合覆写,实现了执行不同类中的speak方法。这就是多态的体现,程序的结果由它运行时期的实际类型决定。上面代码中的new Father() , new Son1() , new Son2()就是在一个数组中创建不同的实际类型。按照常理来讲,数组是相同类型元素的集合,怎么能存放不同类型的东西呢。这就是“父类类型的变量可以指向子类实体”的功劳,上面数组a在逻辑上的类型是Father,但是它的实体分别是Father,Son1,Son2类型,通过不同的实体类型并结合覆写,调用了同种方法却在运行时期产生不同的结果,这,就是多态。
那么多态有什么用呢?通过多态,我们可以在原有功能上直接扩展新功能,而不需要大量的代码改动。比如我们想让上面代码再输出一个I’m student,我们就可以通过 再新建一个Father的子类Student,在Student类中覆写speak,再在数组a中新建一个Student实体 就可以实现,不需要改动其他的代码,也不需要额外定义输出“I’m student”的方法。
我们也可以通过输入来控制哪个类型的实体来执行那种功能。比如这样:
import java.util.* ;public class test{ public static void main(String[] args){ Father a ; int x ; Scanner reader = new Scanner(System.in); System.out.println("please enter 1,2 or 3"); x = reader.nextInt() ; a = switch (x){ case 1 -> new Father() ; case 2 -> new Son1() ; case 3 -> new Son2() ; default -> null ; } ; if( a != null )a.speak(); else System.out.println("please enter 1,2 or 3"); }}class Father{ void speak(){ System.out.println("I'm father"); }}class Son1 extends Father{ void speak(){ System.out.println("I'm son1"); }}class Son2 extends Father{ void speak(){ System.out.println("I'm son2"); }}由于上面代码所实现的功能很简单,通过简单的多分支而不用多态也可以实现。但是当功能复杂时,我们可以通过多态操控程序执行的走向,或者实现其他更复杂的操作。想象力有多大,多态的舞台就有多大!
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分内容可能已过时