JAVA基本语法+面向对象

本文详细介绍了Java的基本语法和面向对象编程。基本语法涵盖类和对象、构造方法、输入输出、数据类型及流程控制;面向对象部分阐述了封装、继承、多态特性,包括一般父子类、密封类、子类构造方法和覆写等内容,帮助读者深入理解Java编程。

目录

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");
    }
}

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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值