JAVA 基础

JAVA 核心基础功能介绍

JAVA是一个面向对象的编程语言,面向对象就是一种抽象开发的思想包含封装、继承、多态,去掉这些概念也就是面向过程。应该是最初很多程序员使用面向过程编程,总结各种经验,对编程过程进行各种改进抽象出的更接近现实世界的最佳实鉴。通过面向对象有助与复杂系统的开发,有效提高编程效率。

  • 抽象 将一类实体的共同特性抽象出来,封装在一个抽象类中
  • 封装 将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。
  • 继承 在一个现有类型的基础上,通过增加新的方法或者重定义已有方法的方式(重载/重写),产生一个新的类型。
  • 多态 通过传递给父类对象引用不同的子类对象从而表现出不同的行为,多态可为程序提供更好的可扩展性,同样也可以代码重用。

类直接的关系:

  1. 继承
  2. 实现
  3. 依赖 一个类A使用到了类B,属于非常弱的关系。a.append(B b)。UML中虚线箭头
  4. 关联 强依赖,长期的关系。B是A的成员变量public class A { private B b = new B();}。UML中实线箭头
  5. 聚合 整体与部分的关系,has-a,是可以分离的,各自有各自的生命周期。比如公司与员工。UML中空心菱形加实线箭头
  6. 组合 是contains-a的关系,比关系比聚合强就是强聚合。整体和部分不可分割。比如人和手。UML中实心菱形加实线箭头

浏览器访问IPV6地址的后端程序,ip地址需要[]括起来。

基础写法

循环

for循环目前有4种写法

1
2
3
4
for (int i=0; i<100; i++) {}
for (String s : list) {}
items.forEach((k,v)->{...});
items.stream().filter(s->s.contains("B")).forEach(System.out::println);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for (int i=0; i<100; i++) {
// break 退出循环
// continue 跳过单次循环
}

//lambada表达式中foreach使用break
List<String> c = Arrays.asList("1", "2", "3", "4");
c.stream().forEach(str ->{
if("3".equals(str)) {
return; //跳出当前循环,继续下一轮
}
System.out.println(str);
});
//lambada表达式中foreach抛异常退出循环
List<String> c = Arrays.asList("1", "2", "3", "4");
try {
c.stream().forEach(str ->{
if("3".equals(str)) {
throw new RuntimeException();
}
System.out.println(str);
});
} catch (Exception e) {

}

try catch return

任何执行try 或者catch中的return语句之前,都会先执行finally语句,如果finally存在的话。
如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的,
编译器把finally中的return实现为一个warning。

try catch后的语句还会继续执行,所以在后面进行return false 那么函数返回的就是false。
try{} catch(){}finally{} return; 按顺序执行
try{ return; }catch(){} finally{} return; 程序执行try块中return之前(包括return语句中的表达式运算)代码;再执行finally块,最后执行try中return;finally块之后的语句return,因为程序在try中已经return所以不再执行。

常用函数

空值判断,最常见的问题之一

1
2
3
4
String[] photo = new String[0]; // 初始化
if (camera.getImages()!=null && camera.getImages().length()>0) {
photo = camera.getImages().split(",");
}

类型转换

  • String.valueOf() Integer.parseInt(String s)
  • 错误用法:强制转换 (String)data //Cannot cast from Integer to String

split

注意: . 、 | 和 * 等转义字符,必须得加 \\

注意:多个分隔符,可以用 | 作为连字符。

public String[] split(String regex, int limit)

regex – 正则表达式分隔符。

limit – 分割的份数

split(" ", -1):

  1. 如果字符串最后一位有值则没有区别
  2. 如果最后N位都是分隔符,split(" ")不会继续切分,split(" ", -1)会继续切分。
  3. split(" ")不会保留空值,但是split(" ")会保留空值
  4. 具体说明可以参看java API文档说明,通过开发工具可以直接查看注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class StringTest {
public static void split() {
String split = "a b c d ";// String split = "a_b_c_d____";//还可以用这个字符串试验split.split("_", -1);
String [] splitArray = split.split(" ");
for(int i=0;i<splitArray.length;i++){
System.out.println(i+"="+splitArray[i]);
}
System.out.println("----------");
String [] splitArrayLimit = split.split(" ", -1);
for(int i=0;i<splitArrayLimit.length;i++){
System.out.println(i+"="+splitArrayLimit[i]);
}
}
public static void splitWithEnd() {
String split = "a b c d d";
String [] splitArray = split.split(" ");
for(int i=0;i<splitArray.length;i++){
System.out.println(i+"="+splitArray[i]);
}
System.out.println("----------");
String [] splitArrayLimit = split.split(" ", -1);
for(int i=0;i<splitArrayLimit.length;i++){
System.out.println(i+"="+splitArrayLimit[i]);
}
}
public static void main(String[] args) {
split();
System.out.println("==========");
splitWithEnd();
}
}
执行结果
0=a
1=b
2=c
3=d
----------
0=a
1=b
2=c
3=d
4=
5=
6=
7=
==========
0=a
1=b
2=c
3=d
4=
5=
6=
7=d
----------
0=a
1=b
2=c
3=d
4=
5=
6=
7=d

public static void main(String[] rags) {
String jsonData = "a,b,c,";
System.out.println(jsonData.split(",").length);
System.out.println(jsonData.split(",", 0).length);
System.out.println(jsonData.split(",", 1).length);
System.out.println(jsonData.split(",", -1).length);
}
-----
3
3
1
4

反射

Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

  • Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
  • Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
  • Constructor :可以用 Constructor 创建新的对象。

Method invoke()调用的例子:

1
2
3
4
5
6
7
8
9
// 1.获取字节码对象
Class<ReflectTest> clazz = (Class<ReflectTest>) Class.forName("vip.techinfo.reflect.ReflectTest");
// 2.获取一个对象
Constructor con = clazz.getConstructor();
ReflectTest intance = (ReflectTest) con.newInstance();
// 3.获取setFF Method对象
Method method = clazz.getMethod("setFF", String.class);
// 4.调用invoke方法来调用
method.invoke(intance, "abc");

List

LinkedList

  • jdk1.6 双向循环列表
  • jdk1.7 去掉了Head节点 变变成双向链表 但是有first last指针方便对头尾插入删除操作,节省了header节点空间
  • 适合频繁插入删除

ArrayList

  • 基于动态数组数组
  • 可以通过下标定位
  • 随机访问 get set优于LinkedList,LinkedList需要移动指针
  • 插入效率没有LinkedList高

demo

1
List<TreeNode> tmpList = new ArrayList<>(treeNodeMap.values()); // Map转List values()

Set

具有去重功能,无序。
需要重写equals()与hashCode()方法,Set集合判断对象是否先等用到这两个方法(相当于通过hashcode生成箱子的编号),可以让工具自动生成

Object的代码:

1
2
3
4
5
6
7
8
9
10
public String toString () {
return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
}

public boolean equals (Object o) {
return this == o;
}

// 内部方法生成hashCode,常规情况与地址无关
public native int hashCode();

hashCode() 方法用于返回字符串的哈希码。
字符串对象的哈希码根据以下公式计算:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
从实现来说,一般的HashCode方法会这样:
return Attribute1.HashCode() + Attribute2.HashCode()…[+super.HashCode()]

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestObject that = (TestObject) o;
return Objects.equals(path, that.path);
}

@Override
public int hashCode() {

return Objects.hash(path);
}

TreeMap

  1. 存入TreeMap的键值对的key是要能自然排序的(实现了Comparable接口),否则就要自定义一个比较器Comparator作为参数传入构造函数。
  2. TreeMap是以红黑树将数据组织在一起,在添加或者删除节点的时候有可能将红黑树的结构破坏了,所以需要判断是否对红黑树进行修复。
  3. 由于底层是红黑树结构(一种自平衡二叉查找树),所以TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是O(log n)。
  4. 由于TreeMap实现了NavigableMap,所以TreeMap有一系列的导航方法。

时间复杂度对比

TreeMap基于红黑树(一种自平衡二叉查找树)实现的是一个有序集合,时间复杂度平均能达到O(log n)。
hashSet(底层通过HashMap实现),hashtable,HashMap是基于散列表实现的,时间复杂度平均能达到O(1),最差O(n)。
ConcurrentSkipListMap是基于跳表实现的,时间复杂度平均能达到O(log n)。
数组 查询的时间复杂度 O(n)
链表 查询的时间复杂度 O(n)
TreeSet O(log(n)) TreeSet是基于TreeMap实现的是一个有序集合,同HashSet、LinkedHashSet一样,它使用TreeMap的键。TreeSet 搜索都是基于TreeMap红黑树搜索。TreeSet中的元素要实现Comparable接口

给定值的比较次数等于给定值节点在二叉排序树中的层数。如果二叉排序树是平衡的,则n个节点的二叉排序树的高度为Log2(n+1),其查找效率为O(Log2n),近似于折半查找。如果二叉排序树完全不平衡,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉排序树的查找性能在O(Log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树。平衡二叉树 查找O(logn) ,二叉查找树 查找最坏O(n)

关键字

  • transient关键字 实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化.transient关键字只能修饰变量,而不能修饰方法和类
  • legth size() length是数组的一个属性,在获取值的时候是按属性的方法获取。而size()是链表的一个方法,用于获取链表的长度
  • <<左移动 >>右移动相当于除以2 ~ 取反
  • static 静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化,按照定义的顺序,顺序初始化,调用内部类的静态成员变量,会先执行静态代码块。静态代码块:全局,不管new多少次,只会初始化1次
  • assert 断言,不符合条件会报错抛出异常

选择单例还是static

只有加载资源/需要初始化并比较慢的类,才去使用单例。单例:多方法,有状态。静态方法:工具,无状态(也能支持有状态),纯函数。其实全局静态变量(类)就是单例模式在语言层面上的一种实现。static变量使用的时候是可以多个类定义相同的对象的。static是在容器加载的时候就已经加载到内存中,所以static方法和变量不宜过度使用,有选择的使用,如果一个方法和他所在类的实例对象无关,那么它就应该是静态的,否则就应该是非静态。因此像工具类,一般都是静态的。如果我们确实应该使用非静态的方法,但是在创建类时又确实只需要维护一份实例时,就需要用单例模式了。从功能上讲:单例模式可以控制单例数量;可以进行有意义的派生;对实例的创建有更自由的控制;比如加载的配置信息,从面向对象的角度考虑应该使用单例模式。
连接池可以使用单例,是说连接池只有一个,池里面的链接可以有多个比如30个。还有Service层与dao层,service层要频繁调用dao层,但是不需要每次都new,dao层没有跟对象相关的值变量,可以用单例实现dao,spring就是这么管理的,dao可以去连接池里取一个链接进行访问数据库,单例支持多线程访问要实现同步得加synchronized

具体使用可以参考:Springboot、Mybatis、Netty等开业框架源码
Netty:ByteToMessageDecoder.MERGE_CUMULATOR
Mybatisplus:

1
2
3
4
SqlCondition 
public static final String EQUAL = "%s=#{%s}";
// SqlHelper(工具类);
MybatisMapperRefresh MybatisMapperRefresh.jarMapper

注意:new一个对象里面的成员变量也会被new一份出来,虽然里面成员变量使用了单例创建但是外层对象new一下还是会导致创建多个不同成员对象。线程比较多的程序可用通过dump查看内存状态

static test

A:随着类的加载而加载
B:优先与对象存在
C:被类的所有对象共享
D:可以通过类名调用

static 随着类的初始化而初始化,随着类的销毁而销毁。 静态变量:方法区的静态区, 成员变量:堆内存。静态变量:属于类,类变量, 成员变量:属于对象,对象变量,实例变量。
修改static值后其他对象获取该值也一样跟着修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StaticTest {
public static String a = "1";
public static String b = "2";
public String getA() {
return a;
}
public void setA(String value) {
a = value;
}
public static void main(String[] args) {
StaticTest staticTest1 = new StaticTest();
StaticTest staticTest2 = new StaticTest();
staticTest1.setA("abc");
System.out.println(staticTest1.getA());
System.out.println(staticTest2.getA());
}
}
/* result:
abc
abc
*/

递归

递归边界,递归公式
递归次数过多容易造成栈溢出

尾递归可以复用栈帧

回调的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface Callback {
abstract void callback(String result);
}
public class CallbackThread implements Runnable {
boolean stop = false;
ShellCallback callback = null;
CallbackThread(Callback callback) {
this.callback = callback;
}
@Override
public void run() {
String result = "abc";
callback.callback(result);
}
}
public class Test { // 如果是通过new Test()创建多个对象,那么这个对象里面的单线程线程池也会有多个,否则就只有一个线程
public static void main(String[] args) throws InterruptedException {
CallbackThread callbackThread = new CallbackThread(new Callback(){ // 在调用的时候设置回调方法执行方式
@Override
public void callback(String result) {
System.out.println(result)
}
});
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-call-runner-%d").build();
ExecutorService singleThreadExecutor = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), namedThreadFactory);
singleThreadExecutor.execute(callbackThread);
//singleThreadExecutor.execute(callbackThread2); 写多份也只会有一个线程
}
}

正则表达式

匹配出.数字.部分内容

1
2
3
4
5
6
String str = "a.b.32.cd.323.f";
Pattern pattern = Pattern.compile("\\.\\d+\\.");
Matcher matcher = pattern.matcher(str);
while(matcher.find()) {
System.out.println(matcher.group());
}

IO

写文件最靠谱的做法

1
2
3
4
5
6
7
8
9
10
11
public static void saveStreamToFile(InputStream in, File file) throws IOException {
FileOutputStream out = new FileOutputStream(file);
int len = 0;
byte[] b = new byte[1024];
while ((len = in.read(b)) != -1) {
out.write(b, 0, len);
}
out.close();
in.close();
}
// 还可以直接用 org.apache.commons.io.FileUtils.copyInputStreamToFile(request.getInputStream(), file);

文件写入与移动

如何避免文件未写完整被解析处理掉:在多个文件夹流水线中分步骤处理,可以先写入临时目录在mv到正式目录,或者写.tmp临时文件写完成重命名成正式文件名进行处理。

java WEB 部署

注意: 部署多个tomcat 8080访问端口跟关闭端口都要改。

队列

如何批量消费队列中数据

1
2
3
4
5
6
7
8
/**
第一个:传入你需要批量消费的队列
第二个:传入一个用来接收批量消费到的数据
第三个:批量消费数据的大小,这里我们给100,即意味着每次消费100条数据
第四个:批量消费的等待的最大间隔,什么意思呢?比如说,我先在队列中只有10条数据,它不到100条,那按道理就不会消费,但是这样显然不合理,所以需要指定当超多多长时间,即使当前队列中数据低于我们设定的阈值也会消费
第五个,这个就很好理解,就是指定第四个参数的单位,是秒是分钟还是小时等等
*/
Queues.drain(blockingQueue, list, 100, 1, TimeUnit.MINUTES);
1
2
3
4
5
<dependency>   
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>

概念理解

值传递与引用传递

新建Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* 值传递: 在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,
* 因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。
*
*/
public class Test {
public static void change(int a) {
a = 20; // 形参
System.out.println(a);
}

public static void main(String[] args) {
int a = 10; // 实参
change(a);
System.out.println(a);
}
}

/***
* 执行结果
* 20
* 10
*/

/**
* 引用传递: "引用"也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,
* 形参和实参指向同一快内存地址,对形参的操作会影响真实内容。
*
* 如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。
* 如果是对引用类型的数据进行操作,分两种情况,一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。
* 一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。
*/
class Test2 {

public static void main(String[] args) {
Dog dog = new Dog(); // 实参
Change.dogToNull(dog);
dog.eat();// 會出現空指針異常嗎?
System.out.println(dog.getAge());
}
}

class Dog {

int age = 0;

void eat() {
System.out.println("dog eat()");
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

class Change {

static void dogToNull(Dog dog) { // 会创建形参→指向实参传入的地址
dog.setAge(5); // 形参与实参指向相同地址空间,内容会同时改变
dog = null; // 改变形参数指向的地址为空,不影响实参
}
}

/***
* 执行结果:不会报空指针异常
* eat()
* 5
*/

class Person {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

/**
* 无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。
* 引用传递,在Java中并不存在
*
*/
class Test3 {

public static void change(Person person) {
System.out.println("传入的person的name:" + person.getName());
person.setName("我是小白");
System.out.println("方法内重新赋值后的name:" + person.getName());
}

public static void change2(Person person) {
System.out.println("传入的person的name:" + person.getName());
person = new Person();// 加多此行代码,并不会改变实参的地址指向.引用传递,在Java中并不存在
person.setName("我是小白");
System.out.println("方法内重新赋值后的name:" + person.getName());
}

// 测试
public static void main(String[] args) {
Person p = new Person(); // 实参
p.setName("我是小明");
change(p); // change2(p);
System.out.println("方法执行后的name:" + p.getName());
}
}

重写

子类重写父类方法的时候还可以继续调用父类的方法

1
2
3
4
5
6
7
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Transport error in " + session, exception);
}
super.handleTransportError(session, exception); // 调用父类的方法
}

开发中可能会遇到的处理

用整数表示下拉框多个值

比如 3的二进制11 代表 1,2 。每个二进制位可以代表一个下拉框输入项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 处理方式循环二进制字符串位数,把各个位数非0的数值计算出来. 1*2 + 1*1 → '2,1'
public String numHandlingCheckBox(String str) {
if (str != null) {
Integer value = Integer.parseInt(str);
String display = Integer.toBinaryString(value);
String resultValue = "";
for (int i=0; i<display.length(); i++) {
int oper = Integer.parseInt(String.valueOf(display.charAt(i))) * (int)Math.pow(2, display.length()-1-i);
if(oper != 0) {
resultValue = resultValue + oper + ",";
}
}
return resultValue;
}
return null;
}

运行

jar包运行方式

  1. java -jar xxx.jar 。 需要jar包中manifest.mf文件包含入口类内容 Main-Class: xxx.xx.XxxMainClass
  2. .sh .bat 执行脚本。可以通过脚本配置各种启动参数环境变量,方便引用第三方包。例子:创建test.sh,lib目录,下面放入a.jar,b.jar,c.jar,启动AppMain.class可以放在其中jar中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #!/bin/bash
    dir=`dirname $0`
    workpath=`cd $dir;pwd`
    JAVA_OPTS="-server"
    JAVA_OPTS="$JAVA_OPTS -Xmx1024m"
    JAVA_OPTS="$JAVA_OPTS -Duser.dir=$workpath"
    for oneJar in ./lib/*.jar
    do cp=$cp:$oneJar
    done
    java $JAVA_OPTS -classpath $cp vip.infotech.AppMain
    echo " =============================================================================="
    echo $dir
    echo $0
    echo $JAVA_OPTS
    echo $cp
    echo $oneJar
    echo "java" $JAVA_OPTS "-classpath" $cp "vip.infotech.AppMain"
    运行结果
    1
    2
    3
    4
    5
    6
    7
    ==============================================================================
    .
    ./test.sh
    -server -Xmx1024m -Duser.dir=/root
    :./lib/a.jar:./lib/b.jar:./lib/c.jar
    ./lib/c.jar
    java -server -Xmx1024m -Duser.dir=/root -classpath :./lib/a.jar:./lib/b.jar:./lib/c.jar vip.infotech.AppMain

war

web项目tomcat下用的是war包。可以用7zip压缩包工具更新war包中的文件。

新特性

声明式编程

类似sql语句。

1
2
3
int count = students.stream()
.filter(s -> s.getAge<18)
.count();

其他功能

  • Runtime.getRuntime().addShutdownHook(shutdownHook);当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作
  • 实体类类型统一,比如对于数据库bigint使用Long类型存储,其他整型都统一使用Integer

JNI

JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。

JNI调用姿势:Java —> JNI —> C/C++(SO库)

如果用eclipse,需将dll或so文件放在项目下,而不是src及其子目录下。
如果用命令行编译,把dll文件放在该包的同目录下。

常见代码有:

1
2
3
4
5
6
7
8
9
10
11
public class Object {
public native int hashCode();
}

public class RandomAccessFile implements DataOutput, DataInput, Closeable {
private native int read0() throws IOException;
}

class Thread implements Runnable {
private native void start0();
}

1、在Java中声明native()方法,然后编译;
2、用javah产生一个.h文件;
3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);
4、将第三步的.cpp文件编译成动态链接库文件;
5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。

JAVA本地方法适用的情况:
1.为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API访问
2.为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用JAVA编写的
3.为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。
4.也就是与Java程序外交互、与操作系统交互

异常可能原因

栈溢出

调用层级过深或者递归调用没有正常退出,函数无限循环调用自己等

gc overhead limit exceeded

由于数据库问题,业务积压也可能导致内存不够用。也就是本来业务处理完内存是要释放的,由于某些原因导致业务无法完全执行完毕,新业务有过来处理。
while循环创建对象?
读取大文件
-XX:-UseGCOverheadLimit 延后出错时间

java.net.SocketTimeoutException: Read timed out

增加超时时长
删掉超时时长配置

tomcat配置,增加disableUploadTimeout="true"或者keepAliveTimeout="100000"

1
2
3
4
5
6
<!--disableUploadTimeout默认值是false-->
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
keepAliveTimeout="100000"
redirectPort="8443" />

ParseException 程序可能共用一个异常

ParseException 时间格式错误是这个异常,Cron表达式格式错误也是这个异常,然后内容里面体现出是哪里报错与报错原因。

SimpleDateFormat会有线程安全问题

因为内部实现中calendar.setTime(date)这条语句改变了成员变量calendar。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解,无状态的好处。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。所以得保证成员变量每个线程使用都是新new出来的而且更不能做成static,不然会有线程安全问题,也就是说这种成员变量问题可以通过new对象解决。
可以线程各自new谨慎做成公共属性。使用同步:同步SimpleDateFormat对象、使用ThreadLocal、抛弃JDK,使用其他类库中的时间格式化类Joda-Time

systemctl stop 关闭失败

可能开启了远程调试导致问题,根据具体情况,考虑是否直接使用kill。

1
2
3
为了调试以及终止导致非法访问的线程,将抛出以下堆栈跟踪
Web应用程序[xxxx]似乎启动了一个名为[mysql-cj-abandoned-connection-cleanup]的线程,但未能停止它。这很可能会造成内存泄漏。线程的堆栈跟踪
但在停止web应用程序时未能将其删除。线程将随着时间的推移而更新,以尝试避免可能的内存泄漏

Failed to copy full contents from

使用FileUtils, this could be due to the fact that the file i am trying to copy is consistantly updating so i might catch it mid update。
Well as you said the file might get updated during your copy process, so you should be required to get a file lock on the file you want to copy.
有建议用 Google guava API Files Class的。或者读写文件加锁的。
用了框架了还得自己加锁用,这个算不算框架BUG呢?

集合工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* lambada test
*/
public class FilterUtils {
/**
* 从map中根据key模糊匹配map项
*/
public static Map<String, Object> parseMapForFilter(Map<String, Object> map, String filters) {
if (map == null) {
return null;
} else {
map = map.entrySet().stream()
.filter((e) -> e.getKey().contains(filters)) // contains实现就是indexOf,还可以用startsWith
.collect(Collectors.toMap(
(e) -> (String) e.getKey(),
(e) -> e.getValue()
));
}
return map;
}

/**
* 从map中根据key模糊匹配返回key Set
*/
public static Set<String> parseSetForFilter(Map<String, Object> map, String filters) {
Set set = null;
if (map == null) {
return null;
} else {
set = map.keySet().stream()
.filter(o -> o.contains(filters))
.collect(Collectors.toSet());
}
return (Set<String>) set;
}
public static void main(String[] args) {
Map<String,Object> params = new HashMap<>(16);
params.put("abc-123","123");
params.put("def-123","123");
Map<String, Object> map = parseMapForFilter(params, "a");
System.out.println(JSON.toJSONString(map));
Set<String> set = parseSetForFilter(params, "de");
System.out.println(JSON.toJSONString(set));
}
}

文件

修改文件权限

好像默认同组是有读的权限的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
File dirFile = new File(dirPath);
dirFile.setReadable(true, false);
dirFile.setExecutable(true, false);
dirFile.setWritable(true, false);



Runtime runtime = getRuntime();
String command = "chmod 770 " + dirPath;
try {
Process process = runtime.exec(command);
process.waitFor();
int existValue = process.exitValue();
if(existValue != 0){
logger.log(Level.SEVERE, "Change file permission failed.");
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Command execute failed.", e);
}
}


private void changeFolderPermission(File dirFile) throws IOException {
Set<PosixFilePermission> perms = new HashSet<PosixFilePermission>();
perms.add(PosixFilePermission.OWNER_READ);
perms.add(PosixFilePermission.OWNER_WRITE);
perms.add(PosixFilePermission.OWNER_EXECUTE);
perms.add(PosixFilePermission.GROUP_READ);
perms.add(PosixFilePermission.GROUP_WRITE);
perms.add(PosixFilePermission.GROUP_EXECUTE);
try {
Path path = Paths.get(dirFile.getAbsolutePath());
Files.setPosixFilePermissions(path, perms);
} catch (Exception e) {
logger.log(Level.SEVERE, "Change folder " + dirFile.getAbsolutePath() + " permission failed.", e);
}
}

类的选择

String StringBuffer StringBuilder

如果是固定值的拼接可以使用String
如果是带循环的字符串不确定的使用StringBuffer或者StringBuilder

内存分析与排查

1
2
3
4
5
6
7
ps -ef|grep java #获取进程号
jps -l -m # JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 jps -ml
jstat -gc 15800 # jc统计,jstat -options 查看相关参数
jmap -heap 150988 # 堆使用情况
jmap -dump:live,format=b,file=dump.hprof 150988 #直接用MAT(Memory Anlysis Tool)打开
jstack -l 150988 # 查看线程快照 jstack -l -F 150988
jinfo -flags 150988 # 查看虚拟机参数配置

java linux内存过高分析

1
2
3
4
5
6
7
8
free -h
top # 发现虚拟内存使用非常高的java进程
ps -ef | grep "进程号"
./jmap -heap 进程号 # java安装目录下bin目录执行
# MaxMetaspaceSize = 17592186044415 MB # 那线程一多,每个线程都要使用一点元空间,每个线程都分配一个 arena,每个都64MB,就会导致巨大的虚拟地址被分配
pmap -x 进程号 # 大量的占用内存大小为65404KB
# 建议控制VIRT的使用,设置环境变量MALLOC_ARENA_MAX,推荐值为CPU的核数,export MALLOC_ARENA_MAX=8
# 一般来说不用太在意VIRT太高,因为你有256TB的空间可以使用。如果你实在需要控制VIRT的使用,设置环境变量MALLOC_ARENA_MAX,例如hadoop推荐值为4,因为YARN使用VIRT值监控资源使用。

64位系统的地址空间不是2^32, 也不是2^64,而一般是2^48。因为并不需要2^64那么大的寻址空间,过大的空间只会导致资源的浪费。64位linux一般使用48位来表示虚拟地址空间,使用40位来表示物理地址空间,可以通过cat /proc/cpuinfo来查看
其中,0x00000000000000000x00007fffffffffff 表示用户空间, 0xFFFF800000000000 0xFFFFFFFFFFFFFFFF 表示内核空间,共提供 256TB(2^48) 的寻址空间。这两个区间的特点是,第 47 位与 48~63 位相同,若这些位为 0 表示用户空间,否则表示内核空间。

通过top可以看到我启动的那么多java进程总共吃了70G内存加上其他进程有70几G,实际我服务器物理内存加交换分区也就只有60几G

java物理内存分析

java memory = direct memory(直接内存) + jvm memory(MaxPermSize +Xmx)
最大堆内存大小:年轻代大小(Eden Space+From Space+To Space)+老年代大小
java应用程序占用内存大小:最大堆内存大小+持久代大小

1
2
3
4
5
./jmap -F -histo 16567 >a.txt  #其输出信息包括类名,对象数量,对象占用大小
./jmap -heap 16554 # 主要看Eden Space的使用率
jmap -histo <pid>|less
jmap -histo <pid>|grep alibaba|sort -k 2 -g -r|less #对象数最多
jmap -histo <pid>|grep alibaba|sort -k 3 -g -r|less #内存最多

java程序吃内存1G或者5G虽然有点多,但是如果基本稳定,其实也是问题不大的,现在服务器16-32G内存都不算多了。

问题

Unable to open socket file: target process not responding or HotSpot VM not loaded
需要使用启动虚拟机相同的用户执行jstack命令sudo -u xxx_user jstack 26814 > a.txt 导入一个文件中查看

扩展

代码性能优化

减少数据库范围,需要访问多次数据库的情况考虑是否可以在重复调用之前一次性调用。也就是获取数据可以在用的时候调用也可以预先调用。哪种效率高用哪种,比如主表与关联附加信息表,调用方法的时候是只传输主表信息进去还是把主表关联表一起获取出来传递到目标方法内给线程池执行呢?,如果这个信息会调用多个任务分别执行的话这个信息提前全部获取好比较合适,如果只会调用一个任务那么可以提前获取,也可以用的时候再获取附加信息影响不大。如果想让任务执行的职责比较单一就是处理数据减少交互那么最好提前准备好数据。还可以考虑这部分附加信息被用到的概率,附加数据是不是有很多份,用到的是哪一份,会不会不同子任务用到的是不同份的附加数据,全部获取容易吗性能消耗怎样。一般情况可能事先获取全部会比较合理一点,用到的时候在获取会简单点比较符合正常思维逻辑。其实都是可以的,有些情况写在一起可能性能更高,有些情况分开性能更高。

代码结构优化

if else优化

  1. 可以把结果提前使用一层if else。有的时候使用一层好一点,有点时候还是使用多层好一点。
  2. 使用枚举
  3. 使用反射+枚举+策略模式
  4. 某些场景可以使用责任链,责任链模式其实就是一个灵活版的if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题
  5. 模板方法判断,利用pplicationContext.getBeansOfType(xx.class); 实现循环判断是否符合条件,需要在具体需要执行的多个实现类中添加是否符合当前类执行的验证方法,会导致需要先创建对象进行判断操作,当然判断操作可以放在静态方法。
  6. 通过反射获取实现某接口的所有实现类名称,调用实现类的某静态方法看是否符合条件,如果符合就通过反射创建该对象进行执行操作。
  7. 条件非常多非常长,而且重复出现,可以定义成一个参数复用。

否定的逻辑,防御式写法,减少嵌套层级,提前return

尽量减少层级提前return,根据情况长语句改成多条短语句执行。
try{}catch 需要赋值字段可以预先定义不进行赋值,在使用的时候在包含到try里面不需要整个代码块都包含进去。

性能分析

List 的length(),直接读取size属性不会重新计算,String的存储结构是数组,调用数组的length也不需要重新计算。首先数组是一个对象类型,数组一旦创建成功它的长度就固定了,所以它可以定义一个变量类型length属性来存储数组的长度,也就是说每次调用lengh属性的时候不会重新的计算数组的长度,也就不会过多的消耗资源;数组在java里是一种特殊类型,有别于普通的“类的实例”对象,java里数组不是类,所以也就没有对应的class文件,数组类型是由jvm从元素类型合成出来的;在jvm中获取数组的长度是用arraylength这个专门的字节码指令的;在数组的对象头里有一个_length字段,记录数组长度,arraylength的实现只需要去读_length字段就可以了,数组还是不可变类型,length大小不能修改,所以获取数组长度也不需要重新计算

其他

可以共用的属性在父类创建定义,当然要避免多线程处理同一个成员变量,继承关系的话new出来,后续没有其他线程进行修改是可以考虑的。

其他API学习

百度SDK API。token有效期是30天,避免重复获取,采取的策略是每次调用接口前做预判断,把当前时间+1天判断是否超过过期时间。

注意事项

主要还是线程的问题,资源分配,有没线程等待读写资源导致程序跟不上节奏,线程池资源全部被吃光。定时任务要能够在周期内执行完才行,不然就得建立队列,一个个执行,最终就很难保障时间了。
定时任务是个线程池,FTP上传又是一个线程池,定时任务线程数得比FTP线程多,FTP线程池得比需要连接的连接数多。任务资源执行的超时时间可以调小一点,比如ftp超时时间等。
还可以通过jstack排查是哪个线程业务一直执行着跑不完。特别是扫描目录上传,第一个线程还没扫描执行完第二个线程又进来扫描执行了,可以通过对象锁、或者类锁或者常量参数等进行判断退出根据具体情况进行加锁,避免周期性进入无法执行完的任务,导致业务积压越来越严重。经过分析好像quartz任务全部被吃满了,全部qz线程都在io或者数据库操作。quartz可以用@DisallowConcurrentExecution注解避免同jobkey的任务并发执行。

问题汇总

was destroyed and cannot be used anymore

java.lang.IllegalStateException: LettuceConnectionFactory was destroyed and cannot be used anymore
at org.springframework.util.Assert.state(Assert.java:76)
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.assertInitialized(LettuceConnectionFactory.java:1263)
at org.springframe work.data.redis.connection.lettuce.LettuceConnectionFactory.getConnection(LettuceConnectionFactory.java:414)

可能原因是配置文件出问题,字符集等。

启动慢问题

openjdk环境中java项目启动慢的问题
可能是openjdk在启动时会去获取hostname,如果未设置就会卡很久。

在服务器上/etc/hosts中加入ip与hostname即可解决,hostname命令查看主机名

echo "21.22.22.xx localhost localhost.localdomain" >> /etc/hosts

现网成功解决启动慢问题

应用文件监听

使用inotify,使用apacheio的文件创建事件是有问题的,文件还没写完就会被当成创建了,需要使用文件写完成关闭事件。
https://github.com/biconou/inotifywait-java
https://github.com/mojadita/INotify/blob/master/INotify/src/es/lcssl/linux/inotify/INotify.java
https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/solaris/classes/sun/nio/fs/LinuxWatchService.java
https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/478a4add975b/src/solaris/classes/sun/nio/fs/LinuxWatchService.java

编译es/lcssl/linux/inotify/INotify

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 在linux解压文件
# 分别编译java文件
cd /home/inotify/INotify-master/INotify/src/es/lcssl/linux/mainclass
javac -cp /home/inotify/INotify-master/INotify/src/ *.java
# 运行
cd /home/inotify/INotify-master/INotify/src/
java es.lcssl.linux.mainclass.TestINotify # 会提示Exception in thread "main" java.lang.UnsatisfiedLinkError: no INotify in java.library.path

make # 在Makefile种增加JDK的路径:JAVA_JDK= /usr/java/jdk1.8.0_131/

export LD_LIBRARY_PATH=/usr/local/lib:/home/inotify/INotify-master/INotify/bin: # 加上lib的路径,或者放在/usr/local/lib中
或者代码增加
static {
System.load("/home/inotify/INotify-master/INotify/bin/libINotify.so");
}

报错 错误 ELF 类: wrong ELF class: ELFCLASS64 (Possible cause: architecture word width mismatch)
原因 编译与运行使用的JDK版本不一致导致的,可能运行使用了系统自带的JDK
改成如下:
/usr/java/jdk1.8.0_131/bin/java es.lcssl.linux.mainclass.TestINotify

正常运行:
[0x40000100]: /tmp // pcp.t02A1E8QB
[0x000100]: /tmp // sh-thd-1667860976
[0x000008]: /tmp // sh-thd-1667860976
[0x000200]: /tmp // sh-thd-1667860976
[0x40000100]: /tmp // pcp.ev9TJoCE1
[0x000100]: /tmp // sh-thd-1667883467
[0x000008]: /tmp // sh-thd-1667883467
[0x000200]: /tmp // sh-thd-1667883467
[0x40000200]: /tmp // pcp.ev9TJoCE1
[0x40000200]: /tmp // pcp.t02A1E8QB

加入到tomcat java项目工程里面,so文件可以放在/usr/lib中,tomcat启动的时候有打印使用的lib路径有哪些。代码的包路径不要改,需要跟c那边的lib匹配的。启动的时候注意有没启动了多个java程序避免lib被多次加载。java代码可以继承早期实现对核心方法进行修改,也可以直接增加该模块,替换部分旧功能,关闭事件来的时候直接调用旧方法。避免放在不同位置,可能导致启动多个java程序不同程序用不同classloader重复价值报错。

maven

对于私有的maven包,可以直接拷贝到本地maven库对应的目录里面。或者修改maven配置文件引用私有仓库。还可以自己搭建一个私有仓库。

参考