JAVA基本语法+面向对象

6976 字
35 分钟
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 father
I'm son1
I'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");
}
}

由于上面代码所实现的功能很简单,通过简单的多分支而不用多态也可以实现。但是当功能复杂时,我们可以通过多态操控程序执行的走向,或者实现其他更复杂的操作。想象力有多大,多态的舞台就有多大!

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

JAVA基本语法+面向对象
https://blog.cplee.cn/posts/java-and-oop
作者
cplee
发布于
2024-03-24
许可协议
CC BY-NC-SA 4.0
最后更新于 2024-03-24,距今已过 800 天

部分内容可能已过时

评论区

Profile Image of the Author
cplee
Hello, I'm cplee.
公告
欢迎来到我的博客!点击下方 “了解更多” 查看更多信息
分类
标签
站点统计
文章
6
分类
3
标签
9
总字数
31,269
运行时长
0
最后活动
0 天前

目录