那一年,我毕业于北邮大学。同年,一种将对软件行业产生重大影响的编程语言在大洋彼岸诞生。它是爪哇。1998年开始学习Java1.2,服务于JavaOrbix,现在Java9已经来了,离Java10也不远了。Java是全栈必备的编程语言之一。说到Java,虽然千言万语,却不知从何说起。老码友从个人角度看Java语言的编程基础。Java虚拟机的真正强项是JVM。JVM是一个抽象的计算机,有指令集、寄存器、垃圾回收堆、栈、存储区、类文件的格式等细节。所有平台上的JVM向上提供与Java字节码完全相同的接口,向下提供适应不同平台的接口,规定了JVM的统一标准,实现了Java程序的平台无关性。这就是常说的,Java是跨平台的,但是跨不同实现的JVM还是有一些区别的。JVM是运行java程序的核心虚拟机,运行java程序不仅需要核心虚拟机,还需要其他的类加载器、字节码验证器以及大量的基础类库。除了JVM,JRE还包含了其他运行Java程序的环境支持。当JVM启动时,类由三个类加载器加载:引导类加载器由JVM实现,它不是java.lang.ClassLoader的子类。它负责加载Java的核心类。sun.boot加载的类。class.path,或者执行java命令时使用-Xbootclasspath选项,或者使用-D选项指定sun.boot.class.path系统属性值扩展类加载器,负责加载扩展中的JAR类包JRE的目录,为引入Java核心类之外的新功能提供了一种标准机制。系统/应用程序类加载器,加载由-classpath或java.class.path系统属性和CLASSPATH操作系统属性指定的JAR包和类路径。类加载器可以通过静态方法ClassLoader.getSystemClassLoader()找到。如果未指定,任何用户定义的类加载器都将使用此类加载器作为其父类。ClassLoader加载Class的大致过程如下:垃圾回收是JVM中的一项重要技术。所谓垃圾回收只是针对内存资源,而JVM对数据库连接、IO读写等物理资源无能为力,所有程序都需要显式释放。为了更快地收集垃圾,可以将对象的引用变量设置为null。垃圾收集是不可预测的。即使调用了对象的finalize(),System.gc()方法也无法确定何时回收,它只是通知JVM。垃圾回收机制可以准确标记存活的对象,准确定位对象之间的关系。前者是完全回收的前提,后者实现合并、复制等功能。现在JVM有多种不同的垃圾回收算法。不同的垃圾回收算法都有典型的场景。垃圾回收算法可以根据内存和CPU使用率的不同进行调整。语法作为一门编程语言,基本的语法都差不多,包括数据类型、运算符、语句、判断和分支、循环、递归等。对于Java的关键字,可以玩文字游戏,排成打油诗。如果是volatiledefault,catchclassshort,抽象包private,throwthisprotected。elsecharbreak,returnsupertrue,instanceofinterfacelong,switchnullnative。而布尔情况,尝试最终静态,扩展假瞬态,抛出无效公共。导入新的float,继续double,实现intbyte,做同步。最后,gotoconst...如果我没记错的话,goto和const是java的保留字而不是关键字。只有搞清楚每个关键词的含义、用法和典型场景,才能算是“磨刀不误砍柴”。java中的基本数据类型有4种8种:整型(int、short、long、byte)、浮点型(float、double)、逻辑型boolean和文本型char。Java中的大部分基本数据结构都体现在java.util中,主要分为Collection和map两个主要接口,程序中最终使用的数据结构都是继承自这些接口的数据结构类。importjava.util.Hashtable;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.HashSet;importjava.util.LinkedHashMap;importjava.util.LinkedHashSet;importjava.util.LinkedList;importjava.util.Stack;importjava。util.TreeMap;importjava.util.TreeSet;importjava.util.Vector;...一般一个空对象占12字节堆空间,一个空String占40字节堆空间,这可能是其中之一推荐stringbuilder的原因。在Java中,类型决定行为。比如byte可以限制数据,但是不能节省内存。在内存中,byte和int占用4个字节的空间。一个对象占用的堆空间大小一般与类中的非静态基本数据类型和引用变量有关。数组中的每个元素都是一个对象,每个对象都有一个16字节的数组对象头。回忆一下栈,Java的堆是运行时的数据区域,类的对象从中分配空间。只有new()方法才能保证每次创建一个新的对象,它们不需要程序代码显式释放。堆由垃圾收集处理。Java的栈访问速度比堆快,栈数据可以共享。栈中存储数据的大小和生存期是必须确定的,它主要存储一些基本类型的变量和对象句柄。(图片来自https://www.programcreek.com/2013/09/top-8-diagrams-for-understanding-java/)可以通过以下方式大致判断不同数据类型的内存占用情况:运行时。getRuntime().gc();Thread.yield();iBefore=Runtime.getRuntime().freeMemory();Classvariable=newclass(参数类别);Runtime.getRuntime().gc();Thread.yield();iAfter=Runtime.getRuntime().freeMemory();System.out.println(iBefore-iAfter);另外,Java中的引用对内存也有不同的影响,主要有:Strongreference:强引用Softreference:软引用Wea??kreference:弱引用Phantomreference:PhantomReferenceinterface抽象类和接口是Java的两个强大工具。抽象类是OOP的共性,接口简单、规范,提高了代码的可维护性和可扩展性。也是软件松耦合的一种重要方式。它对修改关闭,对扩展开放(不同的实现)。界面本身就是开闭原则的体现。Java接口是一系列方法的声明和方法特性的集合。接口只有方法,没有方法实现。说的神秘一点,接口就是一组规则,规定了实现该接口的类或接口必须具备的一组规则,是对相似事物在一定粒度上的抽象表示。interface{[][]}接口是类型转换的前提和动态调用的保证。实现接口完成类型转换,即多重继承,一般用作类型层次结构的起点;动态调用只关心类型,不关心具体的类。接口为不同的类提供了平滑交互的标准。 Java中的类描述了一个实体,包括实体的状态,以及实体可能发出的动作。接口定义了实体可能发出的动作,但只定义了这些动作的原型,没有实现,也没有任何状态信息。所以接口有点像规范或者协议,是一个抽象的概念;而类是实现协议并满足规范的具体实体,是一个具体的概念。从程序的简单理解来看,接口就是函数的声明,类就是函数的实现。请注意,同一语句可能有多种实现。 泛型所谓“泛型”是广义的数据类型,任何数据类型。Java中泛型是指C++模板,本质上是参数化类型的应用,主要包括:泛型类,例如:publicclassMyGeneric{Tobj_a;Vobj_b;MyGeneric(Tobj_1,Vobj_2){this.obj_a=obj_1;this.obj_b=obj_2;}通用接口,例如:interfaceMyInterface>{//...}通用方法,例如:,VextendsT>booleanMyIn(Tx,V[]y)泛型中的类型参数只能用来表示引用类型,不能表示int、double、char等基本类型。但是传递基本类型不会报错,因为它们会被自动装箱到对应的wrapper中班级。类型参数必须是合法的标识符,习惯上使用单个大写字母。一般来说,K表示键,V表示值,E表示异常或错误,T表示一般意义上的数据类型。使用有界通配符,您可以指定参数类型的上限和下限,允许您限制方法可以操作的对象类型。最常见的是指定有界通配符上限,使用extends子句创建。对于实现,只能算是生产者提供元素(get),不能算是消费者获取元素(add)。Java泛型只能用于编译时的静态类型检查,然后编译器生成的代码会抹掉相应的类型信息,这样JVM在运行时才真正知道泛型所代表的具体类型。Java中不允许创建泛型数组,泛型代码也不能直接使用instanceof。使用泛型可以消除显式转换,提高代码重用,并提供更强的类型检查以避免在运行时出现ClassCastException。反射JAVA反射机制处于运行状态,对于任何一个类,你都可以知道这个类的所有属性和方法;对于任何对象,您都可以调用它的任何方法。普通调用需要了解编译前的所有类,包括成员变量、成员方法、继承关系等,反射可以在运行时加载、检测和使用编译时完全未知的类。也就是说,一个Java程序可以在运行时加载一个名字未知的类,并学习它的完整结构。Java反射方法主要分为两大类:Java.lang.reflect.*和Cg-lib工具包。因为反射调用也必须遵循java的可见性协议,所以Class.getMethod方法只能找到类的public方法。如果要获取声明为private的方法对象,需要通过Class.getDeclaredMethod,并在invoke前设置setAccessable(true),以保证调用成功。如果确实需要调用父类方法,可以通过Class.getInterface方法找到父类,然后实例化一个父类对象,然后像调用私有方法一样调用。反射被广泛使用。比如Spring容器的注入就是利用反射通过配置文件读取要实例化的类的名称和属性,然后由spring容器统一实例化,这样既达到了注入的目的,又可以通过容器统一控制bean的作用域和生命周期。J在框架和容器中,比较广泛的是javabean,或者POJO,以及一些持久化对象作为与数据库交互的载体的规范,会有要求:每个字段必须有setXxx/getXxx方法,命名符合到驼峰命名方法,必须声明为公共。包含一个无参数的构造函数。第一项是方便属性值的反映,通过get/set方法。另一个是确保可以通过cls.newInstance()实例化新对象。此外,还有servlets(有init、service、doGet、doPost方法)、filter(有doFilter方法)。这些组件定义的规范是为了通过反射统一调用和管理容器。ava.lang.reflect包还附带了代理模式的实现。静态代理和动态代理都是有趣的东西。很多插件开发都采用了代理模式。注释注释是一种允许在编写代码时直接写入元数据的机制。注解是代码的元数据,其中包含有关代码本身的信息。注释可以用在包、类、方法、变量、参数上。从Java8开始,就有了一种注解,几乎可以放在代码的任何地方,叫做类型注解。被注解的代码不直接受注解的影响,只是根据不同的需要向第三方系统提供自身的信息。注解将被编译到类文件中,并在运行时由业务逻辑处理程序提取。当然,也可以创建在运行时不可用的注解,甚至是只在源文件中可用而在编译时不可用的注解。Java自带的注解可以称为元注解,这些注解由JVM执行。常见的元注解如下:@Retention:用于描述标记的注解如何存储,取值包括:SOURCE、CLASS和RUNTIME。@Target:该注解用于限制可以注解的元素类型。例如:ANNOTATION_TYPE:应用于其他注解CONSTRUCTOR:应用于构造函数FIELD:应用于字段或属性LOCAL_VARIABLE:应用于局部变量。方法:使用方法级注释。PACKAGE:用于包声明PARAMETER:用于方法的参数TYPE:用于类的任何元素。@Documented:被注解的元素将是Javadoc生成的文档中的内容,默认不会成为文档中的内容。此注解可用于其他注解。@Inherited:默认情况下,注解不会被子类继承。标有该注解的注解将被所有子类继承。还有@Deprecated、@SuppressWarnings、@Override等等。Java反射API包括许多方法,用于在运行时从类、方法或其他元素中检索注释。AnnotatedElement接口包含大部分重要的方法,如下所示:getAnnotations():返回该元素的所有注释,包括未在该元素上显式定义的注释。isAnnotationPresent(annotation):检查传入的注解是否存在于当前元素上。getAnnotation(class):根据传入的参数获取指定类型的注解,返回null表示当前元素没有该注解。自己写注解会让代码更简洁。一些库如:JAXB、SpringFramework、Findbugs、Log4j、Hibernate、Junit等,使用注解来完成代码质量分析、单元测试、XML解析、依赖注入等诸多工作。Thread一个JVM相当于操作系统的一个进程。Java线程是进程的实体,是CPU调度调度的基本单位。JVM线程调度器是一种基于优先级的抢占式调度机制。线程有自己的栈和局部变量,但线程之间没有单独的地址空间。一个线程包含以下内容:一个指向当前执行的指令的指令指针,一个栈,以及一个寄存器值的集合,其中定义了描述正在执行的线程的部分。处理器状态值的私有数据区在Java程序中,创建线程有两种方法:派生Thread类并重写run方法,以及通过实现Runnable接口创建。获取当前线程对象的方法是Thread.currentThread()。与继承Thread类相比,实现Runnable接口更适合同一个程序代码的多个线程处理同一个资源,绕过了单一继承的限制,线程池只能放在实现了Runable或可调用类的线程中,一般不会直接放在继承Thread的类中。线程池的基本思想还是一个对象池的思想,开辟一块内存空间,里面存放很多(未死的)线程,线程在池中的执行调度由池来处理经理。当有线程任务时,从池中取出一个,执行完线程对象返回池中,可以避免重复创建线程对象带来的性能开销,节省系统资源。线程池有多种类型:固定大小线程池、单任务线程池、可变大小连接池、延迟连接池、自定义线程池等等。了解Java线程的状态机(new、ready、running、sleeping/blocking/waiting、dying等)对线程的使用很有帮助。(图片来自http://blog.csdn.net/Evankaka/article/details/44153709)在使用任何多线程技术时,都要注意线程安全。虽然在线程安全类中封装了必要的同步机制,使得客户端不需要采取进一步的同步措施,但仍然需要注意资源竞争,即所谓的racecondition。racecondition成立的三个条件:1)两个进程共享变量2)至少有一个进程会修改变量3)另一个进程会在一个进程完成之前介入只要不满足三个条件之一,它可以写一个线程安全的程序。规避一,没有共享内存,就不会出现竞争条件,比如使用独立进程和actor模型。回避2,比如Java中的immutable回避3,不干预,使用协程等协调模式线程,也可以使用表示不方便干预的标志——锁、互斥量、信号量,其实都是状态卡在用.锁的使用包括死锁和无法组合,只能依靠事务内存来解决。通过Java多线程技术,可以提高资源利用率,程序有更好的响应。故障排除零错误是每个程序员的目标。调试是一项繁重的工作。一般减少bug都是从ErrorHandling开始的,ErrorHandling主要体现在Java中的异常处理上。异常处理Java中Exception的继承关系如下:(图片来自https://www.programcreek.com/2013/09/top-8-diagrams-for-understanding-java/)红色部分一定要抓,或者在函数中声明抛出这个异常。其中,throwable是一个有趣的东西。在某些极端情况下,只能通过直接捕获throwable才能获得想要的效果。静态代码分析据说整个软件开发生命周期中30%到70%的代码逻辑设计和编码缺陷都可以通过静态代码分析来发现和修复。然而代码审查往往需要大量的时间消耗和相关知识的积累。因此,使用静态代码分析工具进行代码检查和分析的自动化,可以大大提高软件的可靠性,节省软件开发和测试成本。静态代码分析是指在不运行被测代码的情况下,通过分析或检查源程序的语法、结构、过程、接口等来检查程序的正确性,找出隐藏在代码中的错误和缺陷,如参数不匹配和歧义嵌套语句、不正确的递归、非法计算、可能的空指针引用等。静态代码分析主要基于缺陷模式匹配、类型推断、模型检查和数据流分析等。静态代码分析工具可以自动执行静态代码分析,快速定位代码隐藏的错误和缺陷;帮助我们更专注于分析和解决错误;显着减少逐行检查代码的时间,提高软件可靠性,节省软件开发和测试成本。常用的静态代码工具有checkstyle、findbugs、PMD等,其中Checkstyle更侧重于代码编写格式检查,而FindBugs、PMD、Jtest等侧重于发现代码缺陷,但我个人还是比较喜欢Sonar。在Java中进行故障排除的麻烦之一是内存泄漏。内存泄漏是指无用对象继续占用内存或者无用对象的内存不能及时释放,造成内存空间的浪费。内存泄漏有时并不严重,不易察觉,所以你可能不知道有内存泄漏,但有时很严重,会导致Outofmemory。常用的Java内存分析工具有VisualVM、jconsole、jhat、JProfiler、MemoryAnalyzer(MAT)等。综合考虑可处理的Heapdump的大小和速度、网络环境、可视化分析、内存资源限制,是否免费使用等,推荐工具是jmap+MAT。Java中内存分析的一般步骤如下:Dump出Java应用使用的heap,启动时加入虚拟机参数:-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=path,这样当程序OOM时,会自动在相关路径下生成一个dump文件,使用Java堆分析工具找出对象数量或占用内存过多的对象执行jmap-dump:format=b,file=heap。binpid其中,format=b表示转储的文件为二进制格式,file=heap.bin表示转储的文件名为heap.bin,pid为进程号。需要分析可疑对象与其他对象的引用关系,结合程序源码查找原因。您可以在本地拉取Heapdump,然后使用MAT打开它进行分析。如果Heapdump很大,本地内存不够,可以在服务器上执行shParseHeapDump.shHeapdumpfile,得到分解后的文件,然后拉取到本地,再用MAT打开进行分析。【本文来自专栏作家“老曹”原创文章,作者微信公众号:哦家ArchiSelf,id:wrieless-com】点此阅读更多本作者好文