java基本语法

本篇文章记录学习java时,对其基本语法和思想的一些感悟。
此文不讲基础,只是记录一些大部分书中都没有讲过的感悟。

基本语法

这里挑一些java比较显著的语法亮点说一说。
对应的图: https://www.processon.com/view/link/5ae82d76e4b019d3a91add3c

变量和常量

任何语言都离不开变量和常量。他们是计算机和程序员沟通的最基本元素。
众所周知,计算机的数据不是存储在硬盘中就是在内存里或者其他存储设备。但是无论哪种存储设备都需要取出计算得出结果后数据才有意义。
所以计算机语言就规定了一些获取数据的方式。当然最后都会变为一个常量或者变量。
计算机会根据变量的名字获取变量的地址,然后根据这个地址拿相应的数值展现在电脑屏幕上;
而对于常量,计算机则是直接翻译成内存地址(机器码),然后根据这个地址拿相应的数值展现在电脑屏幕上。
如此看来,常量和变量是唯一可以获取计算机内部存储的数据的方式,如果没有他们,也就没有办法和计算机沟通了。


常量和变量提供了与数据简洁的交互方式,但是同时也带来了新的问题。
随着计算机编程技术的发展,人们发现有一些运算数据的逻辑可以被重复使用。于是就把这些逻辑装在一个“盒子”中,然后给这个盒子起一个名字。当需要使用的时候把数据输入进去就行了。
-(其实这里的“盒子”指的就是函数,java中叫做方法,不同的语言有不同的叫法。“盒子”的名字其实就是方法名。)
当需要使用“盒子”运算的时候,只需要把数据复制一份到“盒子”里就行了。
但是很快人们就发现,当数据过大的时候,复制数据到“盒子”里的这种方式就行不通了。一是复制时间比较长,二是内存空间都不一定够用。
所以为了解决这个问题,编程语言中引入了指针的概念。


指针和引用

指针(pointer)其实是一种数据类型,在后面会详细阐述。指针变量指的是内容为内存地址的变量。
有了这个概念之后,就可以将上面的过大的数据的地址提取出来,存放到一个指针变量里(一般编程语言中把这个操作叫做让X变量指向Y)。当数据需要运算的时候,把这个指针变量复制给“盒子就OK了”。
一般情况下32位的系统指针变量为4B,64位系统为8B(每个编程语言不同,大部分是这个规律)。所以复制一个指针变量的时间就很短了。然后“盒子”会根据这个地址让操作系统把变量拿给他运算,之后输出结果就OK了。


指针变量的引入,极大的提升了程序的运行效率和代码编写的灵活性。而将指针变量发扬光大的当属C语言了。
C语言应该是对指针应用最灵活的语言了,说指针是C的灵魂也不为过。
上面说的盒子的名字,也就是函数的名字,其实是一个常量,当调用这个函数的时候,计算机会把它的名字翻译成地址,找到源码然后编译执行。而C语言觉得这样调用麻烦,反正都是地址,不如放在变量里用的方便,这样就可以随便给函数起名字了。就这样,C语言中的指针开始指向函数,之后又指向了文件再之后又指向了内存中的某一块地方,最后指向了指针。。。


可以看到C的灵活性是非常高的,但是有一利就有一弊。带来的新问题就是指针变量的混乱,可读性降低。
这也是新兴的语言出现之后,大家说C语言门槛高,指针不好学,听指针知难而退的原因。
随着技术的发展,越来越多的人投入到编程事业当中,催生了很多高级程序语言。java就是其中之一。
面对C指针的灵活性和操控难度,java应该算是做的比较完善的语言了。这就是上一篇文章中所说的java做的三件事中的第二件——封装指针为引用。


java看到了指针的灵活性,所以引入了指针,但是考虑到高级编程语言的程序员不应该去操心那些复杂的内存管理,所以把指针封装成了引用,并且构造了内存管理机制。
引用是引用变量的简称,就像指针是指针变量的简称一样。效果跟指针是一样的。不同的是,java在极力避免程序员直接操作引用的内容。
举个例子:
在C语言中定义 int * p = “balabala”; 然后print(“%p”,p);可以打印出指针的地址;
而在java中是打印不出来的。最贴近地址的值也只是个散列码。具体实现原理其实是基于java的数据类型。在后面详解。
有了引用的概念之后,程序员编写程序的灵活性大大提升,多系统协作和并行开发也因此变得更加容易。


类名和方法名

java中的类名(接口、枚举、异常、错误等都是基于类实现,在此和下文中归为类的范畴)、方法名和使用static final修饰的变量都是常量。
指的一提的是,类名指的并不是class关键词后面的那个单词,而是类的完全限定名。
比如在package abc下新建一个class test 那么test类的全名其实是abc.test。计算机也是根据这个名字翻译内存地址的。
方法名分静态和实例方法。静态方法的全名跟类的完全限定名差不多。而实例方法名的前面则是调用这个方法的实例的地址。


分割线1,文章有点长,如果累了可以把这个分割线当做一个书签,下回再看。


操作符

操作符就是操作常量和变量的符号。其实跟变量的思想有点像,用一种符号代替一种运算。
说道这里插一句题外话。前两天学习JVM的时候用到了asm字节码工具。我突然发现字节码中的一条一条指令跟《程序员升职记》里面的操作非常相似。所以给推荐一下这个游戏,在weGame平台上,好像是20块。能提升一下理解代码底层运行原理的兴趣。


访问修饰符

访问修饰符,根据名字就能猜得出来是限制访问的。在java中代码层级的访问的概念主要指对类、方法、变量的访问。
对类的访问修饰符主要有publicprotecteddefaultprivate。权限百度可查,这里不做重点讨论。
这四种修饰符同样可以用在方法和变量上,他们主要对访问权限做控制。


java还规定了对访问方式做控制的修饰符,也可以说成是对内存控制的修饰符,其中之一就是static
static修饰的类、方法和变量都存放在JVM的方法区中,不像成员变量在堆中,局部变量在栈中。
当然static还可以修饰代码块,被static修饰的代码块的执行顺序很有意思,有兴趣的可以去看看JVM相关类加载和初始化的顺序的文章,这里就不做具体介绍了。


另一个叫做final,翻译过来就是最后的的意思,也就是说被这个修饰符修饰的类、方法、变量在实际运行中是不可变的。这里有个概念需要注意的是,不可变的范围是在实际运行中。

变量不能在实际运行中改变的意思是,这个变量初始化值了之后,程序中不能再改变变量的值。那么该如何知道这个变量是否是再一次赋值呢。java规定final修饰的变量只能在变量定义时或者在构造方法中初始化,只有在这两个地方初始化才会认为是初始化,而且只能赋值一次;在其他地方赋值,编译器会直接认为是赋值操作,直接报错。至于既然能判断变量是第几次赋值为什么概要规定必须在定义和构造方法中初始化,这个我也没想通,估计是给广大程序员限定一个规范吧,让大家的代码写的漂亮点。

方法不能在实际运行中改变的意思是,方法定义了之后只能按照本次定义的逻辑来执行。java编译器做的相应处理是不能重写此方法。

类法不能在实际运行中改变的意思和方法相似,定义了之后只能按照本次定义的逻辑来执行。类在实际运行中是一个对象,也就是说一旦根据final修饰的类创建了一个对象,那么这个对象就是他创建时候的样子,不可改变。java编译器做的相应处理是此类不能被继承。

final关键词在变量中和static连用的情况比较多。而修饰方法不多见。修饰类比较常见。比较典型的例子是String类。
这里要说一下,String字符串的底层其实是一个字符数组,也就是说String类型的变量是一个引用变量。而经过上面的说明大家应该明白,如果把一个引用变量传入方法中,方法根据引用变量的内容(地址)改变了变量指向内存的内容,那么在其他地方使用这个引用变量获取到的内容就是改变之后得了。可是java的设计人员认为字符串应该和字符、数字等基本类型(java保留了八种基本类型,后面详述)一样能够非常方便的使用。所以在String类中加上了final修饰符。这样字符串类型的变量使用起来的感觉就跟基本类型一样了。

这样说可能有点蒙,下面贴一段代码说明

public class TestString {

    public static void main(String[] args) {
        int i1 = 2;
        String s1 = "a";
        foo f = new foo();
        test(i1,s1,f);
        System.out.println(i1);
        System.out.println(s1);
        System.out.println(f.s1);
    }

    private static void test(int i1, String s1, foo f) {
        i1++;
        s1 = "b";
        f.s1 = "c";
    }

}
class foo{
    String s1 = "a";
}

这段代码main方法中声明了三个变量,i1是基本类型的变量,s1是字符串类型的引用变量,f是普通的引用变量,指向了foo类。
当这三个参数传入test方法时,java编译器会把这三个参数的值复制一下放在test方法的栈帧中。

这里插一句话,有的人可能对内存管理理解的不是很好,这里有点看不明白。在这只需要记住,一个线程一个栈,一个方法一个栈帧,栈帧是放在栈里的,或者说一个栈里面可以放好多栈帧。

那么这个程序运行起来没有启动其他线程,也就是说只有一个栈,这个栈里有两个栈帧,一是main方法栈帧,而是test方法栈帧。两个栈帧中同有i1、s1和f三个变量。main方法调用了test方法,三个变量发生的变化如下:

i1变量是基本类型变量,值存放在test栈帧中,改变i1的值,test栈帧中的i1值改变。test:i1=3;

s1是字符串类型的变量,s1的值(地址)存放在占中,s1所指向的值(对象)存放在堆中,改变s1的值,程序会根据s1所存的地址到堆中寻找到对应的对象,当要改变对象的值时发现对象是final的,不能改变,所以程序就又新创建了一个对象值为”b”;然后让s1指向这个”b”。test:s1=”b”

f是引用类型的变量,f的值(地址)存放在占中,f所指向的值(对象)存放在堆中,改变f的值,程序会根据f所存的地址到堆中寻找对应的对象,对象没有被final修饰,可以改变。所以执行完成之后f对象中的s1变成了”c”。f对象中的s1同样经历了上面s1的过程。test:f.s1=”c”

此时两个栈帧中的值分别为:
main: i1=2;s1=”a”;f.s1=”c”
test: i1=3;s1=”b”;f.s1=”c”

然后test方法执行完毕栈帧销毁。这个时候只剩下了main: i1=2;s1=”a”;f.s1=”c”;
所以打印结果是
2
a
c
这是一道比较经典的考察新手的题目,其实不需要每次都去走内存管理的原理,只需要记住String类是加了final修饰的,是因为设计人员想把字符串当做基本类型的变量来使用;那么结果就应该和基本类型一致就对了。

再说一个相对常用的:abstract.
这个修饰符可以修饰类或者方法,表示这个方法或者类是抽象的,没有具体实现的。
这个修饰符的出现多半跟团队开发有关。拿数据库举例子。
java做应用层少不了对数据库的访问,有很多数据库厂商都提供了数据库软件,但是每个数据库的驱动不一样。这就导致了每个数据库使用的方式都不一样。这样就很头疼了。于是java的设计者就把数据库厂商聚在一起说:我写一个类,定义一些方法,你们继承我这个类重写我这些方法,这样程序员们就可以用同样的方法、同样的调用形式和参数调用各个数据库了。但是后来,这种方法越用越多,设计者觉得,反正每个厂商实现的方式都不一样,不如方法里就不写逻辑了,可是又觉得一个方法体里面什么都没有怪怪的。所以就产生了abstract。用这个修饰符修饰一下方法,表示这个方法是需要被重写实现的,然后直接使用 ; 结束。类也要加上abstract修饰,表示这个类中有抽象的方法需要实现。但是之后,这种方式越用越多,继承类的方式有单一继承的限制,造成了诸多不便。所以设计者干脆把全都是抽象方法的类改造了一下,默认不写abstract,这个类中的方法全都是抽象的。这个时候在用class就不合适了。所以就起了一个新名字,叫做interface


分割线2,文章有点长,如果累了可以把这个分割线当做一个书签,下回再看。


运算操作符

运算操作符中几个有意思的点说一说:
第一个就是最常用的操作符: .
这个操作符表示根据左侧变量的数据类型,访问其类(和父类)中的方法或者成员变量。所以在eclipse中,当你new了一个对象并调用其方法时,在变量后面打了一个.,这个对象和其父类中的方法和成员变量就会提示出来了。
值得注意的一点是,这个操作符提示和调用方法运行都是在编译期确定的。那么在编译期确定的规则自然是由编译器来做。在变量后面打了一个.之后,编译器会根据这个变量的数据类型名字(常量)找到对应的类文件,然后把这个类文件和其父类文件中的方法和成员变量加载到提示器中。(这一步的具体原理好像是反射,我也不太清楚。)这样,这个变量能调用的方法和成员变量就确定了,如果输入了错误的方法名、参数或者成员变量名,编译器就会报错。至于为什么能加载到父类的方法和成员变量,我放在后面,extends那里说。

但是这个操作符的规则有一个很头疼的地方就是当对象向上造型的时候就不能获取这个对象原本的类的成员。因为编译器是按照变量左侧的类名去找成员的;所以变量是父类的类型就只能找到父类的成员。所以后来发明了重写。


第二个有意思的操作符是++--
其实两者原理相同,我拿++举例子。

int a = 0;
a = a++ + ++a + a++ + ++a;
System.out.println(a);

首先要注意 =运算优先级最低,所以最后执行。(关于优先级可以去查java操作符优先级表)
然后要注意++在变量前表示先进行++操作,后进行其他操作;在变量后边就相反。
最后要注意++操作符是两个操作,首先是将变量值加一,然后将加一后的结果赋值给变量,所以仍然有一个=(赋值操作)。不过java将++封装成了一个操作,所以程序会将加一和赋值给本身的操作执行完毕后再往后执行。
上述程序:
我们将程序第二行=右侧4个a分别称为第一二三四个a。

首先程序检测到=,然后去运算右侧的表达式,运算完成后将结果赋值给a。也就是说不管表达式中对a如何赋值,最后a的值仅取决于表达式的结果。我们管这个决定性的表达式叫做决定表达式。

然后程序运算第一个a,运算为:a++,此时a = 0 。两个运算,一是将a的值取出放入决定表达式中,而是a自增1;++在后面定义,所以先将a的值取出放入决定表达式中,此时决定表达式为:a = 0 + 。然后a自增1,a = 1;

往后执行,检测到第二个a,运算为:++a,此时a = 1。跟上面同样是两个运算。不同的是这次先让a自增1,自增后a = 2;然后将a的值取出放入决定表达式中,此时决定表达式为:a = 0 + 2

以此类推,检测到第三个a,运算为:a++,此时a = 2。跟第一步相同,运算后决定表达式为:a = 0 + 2 + 2;自增后a = 3

最后,第四个a,运算为++a,此时a = 3。跟第二部相同,自增后a = 4;决定表达式为a = 0 + 2 + 2 + 4。再往后检测到;表达式结束,计算决定表达式0 + 2 + 2 + 4结果为8并赋值给a。所以程序打印结果为8。


第三个是java特有的操作符instanceof
这个是因为有了向上造型之后,除了程序真正运行的时候,编译器无法判断对象的真正类型。这个操作符用来检测对象的真正数据类型是什么,用法是 变量名 instanceof 类名。是则返回true,否则返回false。
这个操作符多用于try catch中检测异常类型。这里就不详细讨论了。

数据类型

数据类型说白了就是变量对内存的访问方式。任何一种有变量的编程语言,就有变量对应的数据类型。
比较形象的说:
数据都是存储在内存或者硬盘这样的数据存储设备中的。那么我们并不能直接伸手拿到这个数据,而是需要通过计算机帮我们拿出来,并且显示在屏幕上。
OK,既然是计算机帮我们拿东西,他就一定需要知道两个要素,一个是东西在哪,另外一点是东西是什么样子的。
我们先说东西在哪。这个其实就是前面提到的地址。即使单独定义:int a = 1;这个a变量也有对应的地址。(C语言中可以将a的地址打印出来,但是java不行)
然后就是东西是什么样子的。计算机并不关心这是个什么东西,只关心这个东西是什么样子的。更具体点说,计算机只关心这个东西在内存里占用了几个字节以及这几个字节的数据要怎么拼接成你想要的样子。
这两个因素的规定就是我们所讲的数据类型了。
举个例子: 比如int a = 1; 当我要使用a时。计算机会首先根据a的地址在内存中找到对应位置。然后根据数据类型int确定数据的搬运和拼装方式将数据组装。由于int规定int类型的变量占用4个字节的内存,所以计算机会从a地址的起始位置往后数4个字节,然后将这4个字节的二进制码转换为数字。最后显示在屏幕上。
知道了这个流程就很容易理解,数据类型是变量对内存的访问方式了。

基本数据类型

java保留了8中基本数据类型,分别是:
byte short int long float double boolean char
有意思的是byte short int long 和 char 类型在内存中的存放规则都是按照数字的规则存放的;
不同的是char类型的数据组装方式是按照ASCII码拼接的。所以在.class文件反编译出来的操作码中你会发现,char其实是个数字,但是现实出来是个字符。
这也就印证了上面我们所说的,数据类型由数据的大小和拼装方式组成。
也正是这一点导致了计算机并没有办法对真实的数据进行校验。这就会出现如果取出数据拼接的规则选择错误的话会出现乱码。最常见的就是字符集不正确导致中文乱码问题。

这里还要说一下字符串。虽然字符串是引用实现的,从原理上来讲并不属于基本类型。不过我们上文举过例子说明了,java的设计者就是要把字符串设计成可以当做基本类型使用的数据类型。

自定义数据类型

自定义数据类型的实现,在c中叫做结构体。java将结构体细化了一下,称作类。

这里有一点有意思的事。就是c中的结构体只能存放变量,那么要存放函数就不行了;但是c中的指针可以指向任何东西包括函数;所以只要在结构体中放一个指针问题就解决了。

可是java觉得这样容易造成混乱,就把指针封装成了引用,而引用是不可以指向方法的。所以java就把结构体做了改变,变成了可以存放变量和方法的类。

类是java的基本编程单位,java的多样性由此实现。利用类java封装了接口、抽象类、异常、错误等等面向对象的描述实现单位。同时又利用类制作了一些简单实用的工具。比较典型的有为了解决基本类型的简单操作问题而封装的基本类型包装类;为了解决文件操作问题而制作的文件访问体系等等等等。随着制作的工具越来越多,java就把这些使用频率颇高的类封装成了一个包,这个包就是我们所说的JDK。

类和类之间还可以继承、实现接口。利用这些特性铸造了java面向对象的三大支柱,就是我们总说的继承、封装、多态。


本来想做分割线3的。但是实在是太长了,我写的也累了。所以把脑图右边的java基本思想放在下一篇文章中了。

有疑问和想要讨论的小伙伴欢迎留言。不过本文关于java设计历史的部分纯属虚构,单纯是为了记忆方便提高学习的兴趣而已;如有跟历史相似的地方,别忘了告诉我一声。


  • 12
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值