Java基础语法

92 阅读20分钟

类、对象和接口

面向对象和面向过程

  • Java是一种面向对象的语言,与之相对的就是面向过程
  • 面向过程就是将一个过程拆分为多个步骤,并使用代码实现这些步骤,执行代码完成过程是面向过程的目标
  • 但是面向过程就会存在一个情况,正常需要完成的活动不止一个过程,需要很多个过程配合,并且过程中产生的数据可能下一个过程还会用到,也就是过程之间是需要存在关联的,虽然面向过程的代码也能实现,但是会相对繁琐,而且可能活动不止执行一次,但是每次活动并不一定是完全一样的,参加活动的人,活动时间等都是变量,如果使用面向过程的模式完成,需要每次修改代码,就比较麻烦。
  • 面向对象就是考虑把可能出现的过程还有数据整合到一起,并且为可能存在的变化的因素留下修改空间,整合出来以后就不单单是一个过程或者数据了,被称作面向对象,面向对象是相对面向过程而言,因为整合之后就可以完成多个过程并且有相互配合的数据,就比较像一个实际的物品或者动物了,有自己的属性,有自己的动作。

  • Java也是面向对象的语言,单独只是面向对象还不够,因为就像虽然都是小狗,都会吃东西会叫,但是小狗和小狗之间也是不一样的,不合适用来对应田园犬的对象代码,因为需要对应泰迪就改改改来用或者重新定做一份,那样代码还是比较繁琐。
  • 但是小狗实际上共同点是占了大部分的,泰迪或者田园犬都是小狗都会吃会叫,只是名字不一样,长得不一样,叫声不一样,所以我们可以考虑设计一个模板,模板有上定义可一个名称变量,但是留白,定义一个叫的动作,但是叫声可以作为参数。
  • 这个模板在Java里面就叫类,参照模板复制后给定名称为特别的小狗定制化之后的代码就叫做实例,也就是对象,这个复制的过程就叫类的实例化。

类的定义

类的命名必须要大写字母开头,如果是多个单词组成,只需将每个单词的开头字母也大写就可以了,不推荐使用字符和其他单词全小写

class Dog{
    // 小狗名字
    String name;
    // 小狗叫声
    String barking;
    
    // 狗叫的过程
    String bark(int volume){
        System.out.println("这是狗的叫声:"+barking+"并且音量是:"+volume);
        return barking;
    }
}

类的组成

构造函数
class Dog{
    Dog(){
        System.out.println("你在实例化一只小狗");
    }
    // 小狗名字
    String name;
    // 小狗叫声
    String barking;
    
    // 狗叫的过程
    String bark(int volume){
        System.out.println("这是狗的叫声:"+barking+"并且音量是:"+volume);
        return barking;
    }
}

构造函数是类的必要组成成分,是实例化类的时候执行的函数,是一种定义比较特殊的函数

  • 可以在构造函数函数中定义一些对变量或者其他数据的初始化,为实例的使用做准备
  • 构造函数的名称就是类的名称
  • 构造函数可以有多个,但是需要他的参数不一样
  • 定义类之后默认会有一个无参构造函数,即使没写,系统也会默认有,但是这个默认的构造函数里什么代码都没有,并且如果自定义了一个构造函数,那么这个默认的构造函数就不能用了
成员变量

一个小狗的各种属性提取出来就是这个小狗代码的成员变量,比如上述代码中的名字就是,成员变量有如下两个必要组成成分:

  • 数据类型:首先要明确这个变量的类型,比如字符串还是数值
  • 名字:成员变量的调用就是通过名字来的
private static final String name = "田园犬";

成员变量除了必要项以外还可以有其他成分:

  • 权限修饰:限定变量的调用者范围,比如private就是说这个变量是私有的,仅内部可用
  • 静态修饰:static的作用就是修改变量的归属权,默认情况的变量是划归实例的,但是Java代码在定义类这个模板的时候,也为类划分了一个区域,类在一个程度上也算是一个对象,并且是唯一的,类和实例的关系就相当于父亲和儿子们的关系,这个static的作用就是将变量划分给了类,实例化的时候是不复制这部分代码,所以就不能通过实例调用变量,只能通过类的名字才能找到变量。
  • final:作用是声明这个变量的值只能限定在为初始化的值,也就是“田园犬”,不允许再修改,所以使用final的变量一定是有初始化赋值的,否则不能用。
  • 赋值:成员变量不初始化会赋予默认值,但是默认值只是为了方便代码管理,并无实际意义,但是如果在定义这个类的时候考虑到后续实例化时,变量的初始赋值虽然可能不一样,但是大部分情况会是一样的,就可以在定义类的时候就初始化,那么后续实例化的时候也会把类中初始化的值赋值过去。
成员函数

如果定义了一个小狗的代码,那这个代码应该可以完成如下过程,比如叫,叫的过程我们可以定义为用嘴发出声音,至于发出什么声音可以保留为变量,变量就是这个小狗的特色。在类中,定义这个动作的代码就叫做成员函数,作为一个成员函数,他也是一个过程,定义一个过程就应该有如下必要的组成:

  • 结果:做任何事都需要先明确目的,所以想先定义一个过程就应该明确期望返回的结果,比如狗叫的结果就是一个声音,当然想要代码返回一个声音就比较麻烦了,在上述的类代码中,我们使用返回一个字符串代替。
  • 名称:过程肯定要有一个名字,不然使用它都没法找,在上述代码中就叫bark,狗叫
  • 参数:为啥要有参数,这就是要保证函数的灵活性,每次过程可能出现变化的变量我们就把他提出来作为参数,在调用函数,比如上述代码中,虽然都是同一条狗但是音量是不一样的,每次叫都可以是不同的音量,所以在调用这个动作的时候我们就把它作为参数提取出来,并且每次调用的时候按照实际情况赋值
  • 方法体:这个就是完成动作的过程代码了

除了这几个必要的组成,一个成员函数还可以有其他的修饰,每一个修饰符都是按需加,不一定要一起加:

// 狗叫的过程
public static final String bark(int volume){
    System.out.println("这是狗的叫声:"+barking+"并且音量是:"+volume);
    return barking;
}
  • 权限修饰符:这个修饰符的作用是限定这个函数的调用者范围,比如上面的public就是谁都可以调用
  • 静态修饰符:static的作用就是修改函数的归属权,默认情况的函数是划归实例的,但是Java代码在定义类这个模板的时候,也为他划分了一个区域,类在一个程度上也算是一个对象,并且是唯一的,类和实例的关系就相当于父亲和儿子们的关系,这个static的作用就是将函数划分给了类。
  • final:作用是确定这个函数就是最终版了,因为Java中还有一种机制叫继承,通过一个extends关键字就可以继承一个类,继承之后新类会复制旧类的成员过去,也包括成员函数,这时候新类如果觉得旧类的函数不适用是可以再定义一个同名的函数的,这个叫做重写,新类调用该函数的时候就会默认调用新类的函数,旧类的函数就被覆盖了,但是如果加上final,就是说我这个函数是最终版,不接受重写
代码块
class Dog{
    // 小狗名字
    String name;
    // 小狗叫声
    String barking;
    
    {
        System.out.println("我是代码块,我先执行,构造函数在我后面执行");
    }
    
    // 狗叫的过程
    String bark(int volume){
        System.out.println("这是狗的叫声:"+barking+"并且音量是:"+volume);
        return barking;
    }
}

代码块是通过一个大括号包在一起的,默认情况下实例化的时候会执行一次,但是推荐将代码块中的内容加入构造函数,使得代码有较好的可读性。

不过代码块可以使用static修饰,Java的虚拟机在导入写完的程序文件的并且还未进入运行状态的时候会先去读取每一个类代码文件并且将类代码存入存储区进行类的初始化,这时候如果有static修饰的静态代码块,就会先执行一遍,但是类只初始化一遍,所以这个静态代码块在整个程序运行期间也只执行一次,如果有这个需求的也可以定义静态代码块

类的使用

  • 类具象为对象的时候通过new关键字实现,类的普通成员只能通过对象调用
  • 类初始化时,基本数据变量会初始化为0,引用数据类型的变量会初始化为null
# 定义
public class Dog{
    String name;
    
    String eat(){
    }
}

# 使用
Dog dog = new Dog();
System.out.println(dog.name);
dog.eat();

标识符、关键字、保留字、运算符

标识符是指在代码中出现的命名,只允许包括数字、字母、下划线和$,并且不允许数字开头。

关键字是特殊的标识符,是Java代码中具备特殊含义的字符串,保留字是现在不是关键字但是未来可能变成关键字的字符串,在为变量命名是需要避免与关键字和保留字同名。

自定义的标识符

  • 包的命名:不允许出现大写字母,推荐使用小写字母命名,也可以使用数字、下划线和$,但是要保证可读性与规范性
  • 类命名:大写字母开头,多个单词组合命名时,单词的开头字母都大写
  • 变量和方法命名:使用驼峰规则,即第一个单词的开头字母小写,如果多个单词组合命名则后面单词的开头字母大写
  • 常量命名:全部字母大写,多个单词组合命名时使用下划线分隔

关键字

类权限修饰符

  • public:公共类,允许所有类访问
  • protected:保护类,不允许其他包下面的非子孙类访问
  • default(缺省):不允许其他包的类访问
  • private:私有类,只允许内部类访问

类的类型修饰

  • class:类,只允许定义具备方法体的实体函数,可被直接实例化
  • interface:接口,允许定义具备方法体的实体函数,也允许定义只具备函数声明的抽象函数,但是实例化的实例都是子孙实现类,本身不能被实例化。

类和函数的修饰符

  • static:静态修饰符,用于修饰类内部的变量、函数、代码块、内部类,修饰之后的部分就是属于类的,存储在类的静态存储区,实例化的时候不会复制到实例存储区中,只能通过类引用,不能通过实例引用。
  • final:使用final修饰的类不可被继承;使用final修饰的函数不可被重写;使用final修饰之后变量不可以再发生变更,注意指向对象的变量本身存储的只是地址,final只能限制存储的地址不发生改变,但是不能限制地址指向的区域中的内容发生变更。
  • abstract:抽象修饰符,修饰类后类变为抽象类,允许定义没有方法体的抽象方法,但同时也不可以被直接实例化;定义方法时,方法不允许有方法体,只能是抽象方法。

数据类型

  • byte:8位二进制整型
  • short:16位二进制整型
  • int:32位二进制整型
  • long:64位二进制整型
  • float:32位浮点数,存储区域小,能表示的数值范围和精度都要更小,适合对效率需求大于精确度的场景
  • double:64位浮点数,存储区域大,范围大,精度高,适合科学计算等对精度和范围要求大的场景,计算效率下降
  • char:一位Unicode字符
  • bollean:布尔类型,只有真假两个值
  • void:空

异常

  • try:尝试
  • catch:捕捉
  • throw:抛出指定类型
  • throws:抛出类型声明
  • finally:是否存在都要执行的代码块声明

流程控制

  • if:如果是
  • else:否则
  • for:循环
  • switch:选择
  • case:情况
  • do:做
  • while:当
  • continue:跳过本次循环
  • break:结束循环
  • return:返回

同步与锁

  • sychronized:对一段代码加锁,加锁后代码仅可被一个线程访问,Java6之前的synchronized实现锁是通过jvm底层机制实现,会导致线程阻塞,而Java中线程状态切换是属于内核的操作,需要切换为内核态,需要消耗较大的资源;Java6开始,对synchronized进行了升级,加锁状态分为偏向锁、轻量锁和重量锁,偏向锁是记录线程ID,在线程访问代码块时会优先比对线程ID,如果是只有同一个线程访问代码块,则代码块是不存在加锁过程的,如果出现了其他线程交替访问代码块,则会撤销偏向锁,会采用应用了CAS(compare and swap)的轻量锁机制,这个机制实际也不对代码上锁,会让线程回旋等待,如果等待时间太长才会进入能导致线程阻塞的重量锁机制。
  • atomic:是对变量的同步保证机制,但是不上锁,使用的是CAS机制,当多个线程访问同一个变量的时候,存在多个线程对同一个变量同时做了修改的情况,就会导致除了最后一个提交变更的线程,其他线程的操作都没产生作用,CAS机制就是在提交变更之前,会先比较,变量是否发生了改变,如果发生了,线程会重新执行一遍操作。
  • volatile:这个变量修改了线程修改变量的过程,正常情况下线程会先复制变量到自己的存储区域,修改完载提交回来。使用volatile关键字修饰后,就不产生中间过程了,直接对变量的实际存储区域进行修改。但是这样并不能保证对变量的修改都正确,只能让对变量的修改对所有线程来说都是可见的

其他

  • package:包
  • extends:继承
  • implement:实现
  • true:真
  • false:假
  • instanceof:属于
  • new:实例化
  • this:当前实例
  • super:父类
  • null:空

运算符

  1. 分隔运算符:{[()]}
  2. 单目运算符:++,--,~,!
  3. 算术运算符:*/+-%
  4. 位运算符:<<,>>,<<<,>>>
  5. 比较运算符:>,<,==,!=,>=,<=
  6. 逻辑运算符:&,|,&&,||
  7. 三目运算符:a>b?a:b
  8. 赋值运算符:=,+=,-=,*=,/=,%=

变量

变量是指向存储区中存储的可变元素的名称,命名规则遵循驼峰规则。

变量的分类

变量可以分为基本数据类型和引用数据类型两大类:基本数据类型存放在栈中,运算效率快;引用数据类型的实际值存放在堆中,栈中存放的知识指向变量值在堆存储区中的指针。

变量还可以有如下分类方法

  • 局部变量:函数或代码块中定义的变量
  • 成员变量:类中定义的非静态变量
  • 静态变量:类中定义,但是加了static关键字

基本类型

  • byte:8位整型
  • short:16位整型
  • int:32位整型
  • long:64位整型
  • float:32位浮点数,也叫单精度浮点数
  • double:64位浮点数,也叫双精度浮点数
  • boolean:布尔类型,只有true和false两个值
  • char:一位Unicode字符

引用类型

引用类型的数据值也称作对象,多是类的实例,其中比较特殊的是字符串,定义和使用和基本数据类型类似,但是实际是引用数据类型。

数组
  • 定义:type[] name = new type[size]或者type[] name = new type[]{v1,v2...}
  • 初始化:如果定义时就赋值叫静态初始化,如果定义时只定义了数组大小,叫动态初始化,虚拟机会为数组分配默认值
  • 使用:数组的下标从0开始,通过name[下标]的方式调用数组
  • 工具类:Java有内置的工具类Arrays,包含了sort()等静态数组方法

变量的声明与使用

int a = 10;double b;short c = 1;

b = 10;a=(int)b;b=b+c

  • 声明:Java中变量定义的时候需要先确认变量类型,并且后续赋值时选用的数据类型是有限制的
  • 作用域:局部变量从变量声明开始到包裹变量的最里层代码块的结束位置;成员变量的作用域在实例中;静态变量的作用域则是全局。
  • 基本类型转换:基本类型的转换可以从低位数到高位数,或者从同位数整型转到浮点数可以唤作正向,正向直接转换,但是反向转化的话需要强制转换,并且因为存储区域变小的缘故,数值还会发生如四舍五入这样的变化或者直接数据出错;在一个算式中,如果是位数不一样,低位数的会自动发生正向的转换
  • 引用数据类型转换:引用数据类型也是和基本数据类型类似,存在一个方向性,它的正向是子孙类到父类,反向也是需要强制转换,并且还可能出现子类拥有的变量或者函数父类没有,实际调用时出错的情况

流程控制

顺序结构

Java代码默认顺序执行

选择结构

if+else

if可以单用,else必须搭配if使用,else会默认去匹配else前面最近的if,这是if-else语句嵌套的规则

// 一重
if(10>1){
    System.out.println("执行我");
}
// 两重
if(10>1){
    System.out.println("执行我");
}else {
    System.out.println("我跳过");
}
// 多重
if(10>12){
    System.out.println("我跳过");
}else if(10>9){
    System.out.println("执行我");
}else {
    System.out.println("我跳过");
}
// 嵌套
if(10>1){
    System.out.println("执行我");
    if(10>9){
        System.out.println("执行我");
    }else {
        System.out.println("跳过我");
    }
}

switch+case

switch后跟一个变量,case后跟一个值,当变量和值匹配上,就执行相应case后的代码,这个变量的类型只能是基本数据类型和字符串,case语句的末尾需要加一个break阻断,不然switch的判断还会往后继续进行

int a = 10;
switch(a){
    case 10:
        System.out.println("执行我");
        break;
    case 9:
        System.out.println("跳过我");
        break;
    case 8:
        System.out.println("跳过我");
        break;
    default:
        System.out.println("没有匹配上时执行我");
}

循环结构

for循环

基础for循环:for(首次执行代码:判断代码:循环执行代码){循环主体},首次执行代码一般是循环体中的变量初始化,循环执行代码一般是每次循环结束对循环体中变量的变更,判断代码则是判断此次循环是否执行,一般是依据一直在变更的循环体中的变量,常用情形如下

for(int i = 0;i<10;i++){
    System.out.println("循环执行我");
}

增强for循环:依次取出一个集合中的元素,并进行循环

String[] ss = ["a","b","c"];
for(String s : ss){
    System.out.println("我是本次取出的元素:"+s);
}

while循环

while(判断){循环体},如果判断为真就会执行循环体,一般情况判断的依据是一个变量,而在循环体中会每次对这个变量进行操作。

a=1;
while(a<10){
    a++;
    System.out.println("执行我");
}

上述的循环中需要先进行判断才能执行循环体,如果一开始a就小于10,循环体就不会执行,如果需要先执行一次再开始判断就需要改为do{循环体}while(判断)

a=1;
do{
    a++;
    System.out.println("执行我");
}while(a<10)

类的特性

Java允许将子类实例赋予父类类型的引用

  • 多态:再通过继承和实现,再重写和重载函数,就可以实现父类引用调用同一个函数,但是因为赋予的实例不一样,方法的函数可以不一样
  • 封装:当我们有隐藏代码具体实现逻辑的需求时,我们就可以引用封装的特性了,定义一个接口,然后定义实现类实现这个接口中的具体函数;定义接口时赋予实现类的实例,通过指向接口的引用只能看见接口,找不到具体实现。并且这个有个好处是调用代码与实现代码之间耦合度降低

继承与实现

  • 定义有多个相似点的实例是一个大量重复浪费时间的工作,所以以类为模板,通过多个new就能得到多个大同小异的实例,这是类的一大好处;同理,类和类之间也可能存在大量相似点,所以为减少重复工作,同时当这部分相似类的相似点出现变更的时候不需要一个一个地修改,只需要修改父类,可以避免修改遗漏的风险,我们也可以考虑将大量的相似点整合到一个模板中,然后其他类也使用某个关键字就可以复制过来相似点,然后得到多个大同小异的类,这就是继承。
  • 继承就是通过extends关键字,继承父类,然后子类不需要再定义父类中可传递给子类的非private成员,只需要定义子类的异,能简化工作,同时比如eat过程需要修改时,只需要修改Animal类即可
public class Animal{
    string name;
    void eat(){};
}

public class Dog extends Animal{
    void mindHouse(){};
}
public class Pig extends Animal{
    String taste;
}
  • 因为Java的继承只能继承一个父类,在Java中,多继承只能通过实现接口的方式来,Java可以实现多个接口,实现接口时需要将抽象方法全部具象化
public interface Animal{
    string name;
    void eat();
}

public interface Pet{
    void cry();
}
public class Pig implement Animal,Pet{
    String taste;
}

重写override、重载overload

  • 重写:是指在子类重新定义了一个方法声明完全与父类完全一致,但是方法体可能不同的成员函数,这样通过子类的实例调用这个名称的函数的时候,就会默认调用子类的成员函数,好处是子类既可以继承父类的方法,也不被限定方法不可修改,重写有一个要求——限定修饰符的范围可以更大,可以一样,但是不能更小
  • 重载:是指在同一个类中,可以定义两个同名方法,但是同名方法的参数不能完全一样,参数的数量、顺序、类型等任意不一样都可以,好处是可以根据参数不同调用不同的方法体,对调用者友好
public class Father{
    void eat(int number){};
}
public class Son extends Father{
    @override
    void eat(int number){};
    
    # 重载
    void eat(double number){};
}