全栈必备 Java 基础

那一年,从北邮毕业,同一年,在大洋的彼岸诞生了一门对软件业将产生重大影响的编程语言,它就是——Java。1998年的时候,开始学习Java1.2,并在Java Orbix 上做服务,而如今Java 9 已经来了,而且 Java 10 也已经不远了。

对一个全栈而言,Java 是必备的编程语言之一。 而谈到Java,虽万语千言却不知从何开始,老码农从个人的角度看一下Java 语言的编程基础。

虚拟机

Java 真正牛X的地方就在于JVM。JVM是一个抽象的计算机,具有指令集、寄存器、垃圾回收堆、栈、存储区、类文件的格式等细节。所有平台上的JVM向上提供给Java字节码的接口完全相同,但向下提供适应不同平台的接口,规定了JVM的统一标准并实现了Java程序的平台无关性。这就是常说的,Java的跨平台,但跨越不同实现的JVM时还是有些许不同的。

JVM是运行java程序的核心虚拟机,而运行java程序不仅需要核心虚拟机,也需要其他的类加载器,字节码校验器以及大量的基础类库。JRE除了包含JVM之外还包含运行Java程序的其他环境支持。

当JVM启动时,由三个类加载器对类进行加载:

bootstrap classloader 是由JVM实现的,不是java.lang.ClassLoader的子类 ,负责加载Java的核心类,其加载的类由 sun.boot.class.path指定,或者在执行java命令时使用-Xbootclasspath选项, 还可以使用-D选项指定sun.boot.class.path系统属性值

extension classloader ,它负责加载JRE的扩展目录中JAR的类包,为引入除Java核心类以外的新功能提供了一个标准机制。

system/application classloader,加载来自-classpath或者java.class.path系统属性以及CLASSPATH操作系统属性所指定的JAR包和类路径。可以通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。

ClassLoader加载Class的一般过程如下:


这里写图片描述

垃圾回收是JVM 中的一项重要技术。所谓垃圾回收只是针对内存资源,而对于物理资源如数据库连接、IO读写等JVM无能为力,所有程序中都需要显式释放。为了更快回收垃圾,可以将对象的引用变量设为null。垃圾回收具有不可预知性,即使调用了对象的finalize() ,System.gc()方法也不能确定何时回收,只是通知JVM而已。垃圾回收机制能精确标记活着的对象,能精确定位对象之间关系,前者是完全回收的前提,后者实现归并和复制等功能。现在JVM有多种不同的垃圾回收算法实现,不同的垃圾回收算法都有着典型的场景, 根据内存和cpu使用的不同可以对垃圾回收算法进行调整。

语法

作为一种编程语言,基本语法都是类似的,包括数据类型,操作符,语句,判断和分支,循环,递归等。

对于Java 的关键字可以做个文字游戏,排列成打油诗。

if volatile default, catch class short,
abstract package private, throw this protected.

else char break, return super true,
instanceof interface long, switch null native.

while boolean case, try final static,
extends false transient, throws void public.

import new float, continue for double,
implements int byte, do synchronized.

finally, goto const......

如果没有记错的话,goto 和 const 是 java 的保留字而不是关键字。弄清楚每个关键字的意义、用法、典型场景等,才算是“磨刀不误砍柴功”。

数据

java 中的基本类型有4类8种:整型(int, short, long, byte),浮点型( float, double),逻辑型 boolean和 文本型 char。

Java中的基本数据结构大多在java.util 中体现,主要分为Collection和map两个主要接口,而程序中最终使用的数据结构则是继承自这些接口的数据结构类。

import java.util.Hashtable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Vector;
.....

一般的,一个空的对象需要占用12字节的堆空间,一个空的String就要占用40字节的堆空间,这或许就是推荐用stringbuilder的一个原因吧。在Java中,类型决定行为,例如byte可以起到限制数据的作用,但是并不能节约内存,在内存中byte和int一样是占用4字节的空间。一个对象的占用堆空间的多少一般与类中非static的基本数据类型和引用变量有关。每一个数组中的元素都是一个对象,每一个对象都有一个16字节的数组对象头。

回忆一下堆栈,Java 的堆是一个运行时数据区,类的对象从中分配空间。只有通过new()方法才能保证每次都创建一个新的对象,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的。Java的栈存取速度比堆要快,栈数据可以共享,存在栈中的数据大小与生存期必须是确定的,主要存放一些基本类型的变量和对象句柄。


(图片来自 https://www.programcreek.com/2013/09/top-8-diagrams-for-understanding-java/)

可以通过如下的方式粗略的判断不同数据类型的内存使用:

Runtime.getRuntime().gc();
Thread.yield();
iBefore = Runtime.getRuntime().freeMemory();
类 变量 = new 类(参数类别);
Runtime.getRuntime().gc();
Thread.yield();
iAfter = Runtime.getRuntime().freeMemory();
System.out.println(iBefore-iAfter); 

Java 中的引用对内存也有着不同的影响,主要包括:

  • 强引用: strong reference
  • 软引用: soft reference
  • 弱引用: weak reference
  • 虚引用: Phantom Reference

接口

抽象类和接口是Java 的两大利器, 抽象类是OOP 的共性,而接口则简单规范,提高了代码的可维护性和可扩展性,同时是软件松耦合的重要方式。对修改关闭,对扩展(不同的实现implements)开放,接口本身就是对开闭原则的一种体现。

Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法而没有方法的实现。弄一点玄虚,接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则,是在一定粒度上同类事物的抽象表示。

<修饰符>interface<接口名>{ 
[<常量声明>] 
[<抽象方法声明>] 
} 

接口是类型转换的前提和动态调用的保证。实现某一接口就完成了类型的转换也就是多重继承,一般用来作为一个类型的等级结构的起点;动态调用则只关心类型,不关心具体类。接口可以为不同类顺利交互提供标准。

Java中的类描述了一个实体,包括实体的状态,也包括实体可能发出的动作。而接口定义了一个实体可能发出的动作,但只是定义了这些动作的原型,没有实现,也没有任何状态信息。所以接口有点象一个规范、一个协议,是一个抽象的概念;而类则是实现了这个协议,满足了这个规范的具体实体,是一个具体的概念。

从程序角度简单理解,接口就是函数声明,类就是函数实现。需要注意的是同一个声明可能有很多种实现。

泛型

所谓“泛型”,就是宽泛的数据类型,任意的数据类型。Java 中的泛型是以C++模板为参照的,本质是参数化类型的应用,主要包括:

泛型类,例如:

public class MyGeneric<T,V> {
T obj_a;
V obj_b;
MyGeneric(T obj_1,V obj_2){
this.obj_a = obj_1;
this.obj_b = obj_2;
}

泛型接口,例如:

interface MyInterface<T extends Comparable<T>>{
//...
}

泛型方法,例如:

<T extends Comparator<T>, V extends T> boolean MyIn(T x, V[] y)

泛型中的类型参数只能用来表示引用类型,不能用来表示基本类型,如 int、double、char 等。但是传递基本类型不会报错,因为它们会自动装箱成对应的包装类。类型参数必须是一个合法的标识符,习惯上使用单个大写字母,通常情况下,K 表示键,V 表示值,E 表示异常或错误,T 表示一般意义上的数据类型。

使用有界通配符,可以为参数类型指定上界和下界,从而能够限制方法能够操作的对象类型。最常用的是指定有界通配符上界,使用extends子句创建。 对于实现了<? extends T>的集合类只能将它视为生产者向外提供元素(get),而不能作为消费者来对外获取元素(add)。

Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就知道泛型所代表的具体类型。在Java中不允许创建泛型数组,无法对泛型代码直接使用instanceof。

使用泛型,可以消除显示的强制类型转换,提高代码复用,还可以提供更强的类型检查,避免运行时的ClassCastException。

反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法。普通调用需要在编译前必须了解所有的class,包括成员变量,成员方法,继承关系等。而反射可以于运行时加载、探知、使用编译期间完全未知的类。也就是说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造。

Java反射的方式主要分为两类:Java.lang.reflect.*和Cg-lib工具包。

因为在反射调用中同样要遵循java的可见性规约,因此Class.getMethod方法只能查找到该类的public方法。如果要获取声明为private的方法对象,则需要通过Class.getDeclaredMethod,而且在invoke前要设置setAccessable(true)才能保证调用成功。如果的确需要调用父类方法,可以通过Class.getInterface方法查找父类,再实例化一个父类对象,然后按照调用private Method的方式进行调用。

反射的应用广泛,例如Spring容器的注入,就是运用了反射的方式,通过配置文件读取欲实例化的类的名称,属性,然后由spring容器统一实例化,既达到了注入的目的,又可以通过容器统一控制bean的作用域、生命周期等。J

在框架和容器中,比较广泛的就是java bean的规范,或者POJO,以及一些作为与数据库交互载体的持久化对象,都会有要求:

  1. 每个field都要有setXxx/getXxx方法,命名符合驼峰命名法,且需要声明为public的。
  2. 含有一个无参的构造方法。

第一条就是为了方便反射属性值,通过get/set方法。另一条是为了保证可以通过cls.newInstance()实例化一个新对象。 另外还有servlet(要有init、service、doGet、doPost方法),filter(要有doFilter方法)。这些组件定义的规范就是为了容器可以通过反射的方式进行统一调用和管理。

ava.lang.reflect包中还自带了代理模式的一个实现,静态代理和动态代理都是有意思的事, 很多插件化开发都使用了代理模式。

注解

注解这种机制允许在编写代码的同时可以直接编写元数据。注解就是代码的元数据,包含了代码自身的信息。

注解可以被用在包,类,方法,变量,参数上。自Java8开始,有一种注解几乎可以被放在代码的任何位置,叫做类型注解。被注解的代码并不会直接被注解影响,只会向第三系统提供关于自己的信息以用于不同的需求。注解会被编译至class文件中,而且会在运行时被处理程序提取出来用于业务逻辑。当然,创建在运行时不可用的注解也是可能的,甚至可以创建只在源文件中可用,在编译时不可用的注解。

Java自带的内建注解可以叫元注解,由JVM 对这些注解进行执行。常见的元注解如下:

@Retention:用来说明如何存储已被标记的注解,值包括:SOURCE, CLASS和RUNTIME。

@Target:这个注解用于限制某个元素可以被注解的类型。例如:

ANNOTATION_TYPE :应用到其他注解上
CONSTRUCTOR:使用到构造器上
FIELD:使用到域或属性上
LOCAL_VARIABLE:使用到局部变量上。
METHOD:使用到方法级别的注解上。
PACKAGE:使用到包声明上
PARAMETER:使用到方法的参数上
TYPE:使用到一个类的任何元素上。
@Documented:被注解的元素将会作为Javadoc产生的文档中的内容,都默认不会成为成为文档中的内容。这个注解可以对其它注解使用。

@Inherited:在默认情况下,注解不会被子类继承。被此注解标记的注解会被所有子类继承。

还有 @Deprecated,@SuppressWarnings,@Override等等。

Java反射API包含了许多方法来在运行时从类、方法或者其它元素获取注解的手段。接口AnnotatedElement包含了大部分重要的方法,如下:

  • getAnnotations(): 返回该元素的所有注解,包括没有显式定义该元素上的注解。
  • isAnnotationPresent(annotation): 检查传入的注解是否存在于当前元素。
  • getAnnotation(class): 按照传入的参数获取指定类型的注解。返回null说明当前元素不带有此注解。

自己写个注解,会让代码变得简洁。一些类库如:JAXB, Spring Framework, Findbugs, Log4j, Hibernate, Junit等,使用注解来完成代码质量分析,单元测试,XML解析,依赖注入和许多其它的工作。

线程

一个JVM 相当于操作系统的一个进程,Java线程是进程的一个实体,是CPU调度和分派的基本单位,JVM线程调度程序是基于优先级的抢先调度机制。 线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程包含以下内容:

  • 一个指向当前被执行指令的指令指针
  • 一个栈
  • 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值
  • 一个私有的数据区

在 Java程序中,有两种方法创建线程:对 Thread 类进行派生并覆盖 run方法和通过实现Runnable接口创建。获取当前线程的对象的方法是Thread.currentThread()。实现Runnable接口相对于继承Thread类而言,更适合多个相同的程序代码的线程去处理同一个资源,绕过单继承限制,而且线程池只能放入实现Runable或callable类线程,一般不直接放入继承Thread的类。

线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。线程池分好多种:固定尺寸的线程池、单任务线程池、可变尺寸连接池、延迟连接池、自定义线程池等等。

理解Java线程的状态机(新建,就绪,运行,睡眠/阻塞/等待,消亡等)对于线程的使用很有帮助。


图片来自http://blog.csdn.net/Evankaka/article/details/44153709

在使用任何多线程技术的时候,都要关注线程安全。尽管线程安全类中封装了必要的同步机制,从而客户端无须进一步采取同步措施,但还是要关注一下资源竞争即所谓的竞态条件。竞态条件成立的三个条件:

  1. 两个处理共享变量
  2. 至少一个处理会对变量进行修改
  3. 一个处理未完成前另一个处理会介入进来

只要三个条件有一个不具备,就可以写线程安全的程序了。 规避一,没有共享内存,就不存在竞态条件了,例如利用独立进程和actor模型。 规避二,比如Java中的immutable 规避三,不介入,使用协调模式的线程如coroutine等,也可以使用表示不便介入的标识——锁、mutex、semaphore,实际上是使用中的状态牌。锁的使用问题包括死锁和无法组合,只能寄托于事务内存来奢望解决了。

通过Java多线程技术,可以提高资源利用率,程序拥有更好的响应。

排错

Zero Bug 是每个程序员的目标, debug 是项繁重的工作,减少bug一般从Error Handling 开始,在Java 中主要体现在异常处理。

异常处理

Java 中 Exception的继承关系如下图:

图片来自https://www.programcreek.com/2013/09/top-8-diagrams-for-understanding-java/

红色部分为必须被捕获,或者在函数中声明为抛出该异常。其中,throwable 是一个有趣的东西, 在某些极端情况下, 直接catch throwable 才能得到想要的效果。

静态代码分析

据说,在整个软件开发生命周期中,30% 至 70% 的代码逻辑设计和编码缺陷是可以通过静态代码分析来发现和修复的。但是,code review 往往要求大量的时间消耗和相关知识的积累,因此使用静态代码分析工具自动化执行代码检查和分析,能够极大地提高软件可靠性并节省软件开发和测试成本。

静态代码分析是指无需运行被测代码,仅通过分析或检查源程序的语法、结构、过程、接口等来检查程序的正确性,找出代码隐藏的错误和缺陷,如参数不匹配,有歧义的嵌套语句,错误的递归,非法计算,可能出现的空指针引用等等。静态代码分析主要是基于缺陷模式匹配,类型推断,模型检查和数据流分析等。

通过静态代码分析工具可以自动执行静态代码分析,快速定位代码隐藏错误和缺陷;帮助我们更专注于分析和解决bug;显著减少在代码逐行检查上花费的时间,提高软件可靠性并节省软件开发和测试成本。

常用的静态代码工具有checkstyle,findbugs,PMD等,其中Checkstyle 更加偏重于代码编写格式检查,而 FindBugs,PMD,Jtest 等着重于发现代码缺陷,但个人还是喜欢Sonar。

内存泄漏

在Java中排错的一个麻烦就是内存泄露。内存泄漏是指无用对象持续占用内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费。内存泄露有时不严重且不易察觉,这样可能不知道存在内存泄露,但有时也会很严重,会引发Out of memory。

常用的Java内存分析工具有VisualVM、jconsole、jhat、JProfiler、Memory Analyzer (MAT)等。考虑能处理的Heapdump大小及速度,网络环境,可视化分析,内存资源限制,是否免费使用等,推荐的工具为jmap + MAT。

Java中内存分析的一般步骤如下:

  1. 把Java应用程序使用的堆dump下来,启动时加虚拟机参数: -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=path,这样在程序发生OOM时,会自动在相关路径下生成dump文件

  2. 然后使用Java heap分析工具,找出对象数量或占用内存太多的对象 执行jmap
    -dump:format=b,file=heap.bin pid 其中,format=b,表示dump出来的文件是二进制格式,file=heap.bin,表示dump出来的文件名是heap.bin,pid是进程号。

  3. 需要分析嫌疑对象和其他对象的引用关系,结合程序的源代码,找出原因。
    可以将Heapdump拉到本地,使用MAT打开进行分析。如果Heapdump较大,本地内存不够,可以在服务器上执行sh
    ParseHeapDump.sh Heapdumpfile,得到分解后的文件,然后拉到本地,再使用MAT打开,就可以进一步分析了。

不是小结的小结

Java 博大精深,技术栈很多,有关Java 的书也是浩如烟海,这里只谈到了个人认为的一些Java基础,包括 虚拟机,语法,数据结构,接口,泛型,反射,注解,线程,排错等几个部分。

“勿在浮沙筑高塔”,基础扎实一点,才可能走的远一点。《深入分布式缓存》一书中的代码实现几乎都是Java的, 有兴趣的朋友可以看一下。


这里写图片描述

京东上的购买链接,呵呵。


这里写图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,014评论 11 349
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • (1) 思思是一个职场妈妈。 “这次客户对我们很满意,做得不错!”提案后,领导对思思说。 “是您指导的好”。思思微...
    郭小果子阅读 271评论 2 1
  • 像我们这代人,计划经济伴随了大半辈子,从统购统销开始一直到票证制度的终结,黏黏糊糊四十余载。在物质供应捉襟见肘的情...
    山水依情阅读 206评论 0 0
  • 突然听到花开的声音, 听到雨滴的吟唱, ...
    爱之涯阅读 209评论 0 3