java学习笔记 head first java

java学习笔记 head first java文章目录golangtojavaHeadFirstJavagolangtojavagolang工程师,最近开始学习一些javaHeadFirstJavainstanceof相当于断言Dogd=newDog()Objecto=dif(oinstanceofDog){ Dogd=(Dog)o}interface在java和golang中基本一致,java中的interfece是一个100%抽象类,所有函数都是抽象的。必须要用implements显

大家好,又见面了,我是你们的朋友全栈君。

golang to java

golang工程师,最近开始学习一些java

Head First Java

instanceof相当于断言

Dog d = new Dog()
Object o = d
if (o instanceof Dog) { 
   
	Dog d = (Dog)o
}

interface在java和golang中基本一致,java中的interfece是一个100%抽象类,所有函数都是抽象的。必须要用implements显式指定一个接口,(可以是多个吗?可以,用逗号分隔)

public interface Pet { 
   
	public abstract void beFriendly();
	public abstract void play();
}
public class Dog extends Canine implements Pet,Binterface,Cinterface { 
   
	public void beFriendly(){ 
   
	}
	public void play(){ 
   
	}
}
type Pet interface { 
   
	beFriendly()
	play()
}

golang中不需要implements,java必须要implements

final

final也可用于修饰非静态变量,表达一种不能变的概念,包括实例变量,局部变量,或者方法的参数。(不变性上类似const),还可以防止方法的覆盖或创建子类(继承体系中的末端)。

一些与众不同的设计

staic初始化

加载类时,会执行static{xxx},这个可以初始化staic final变量


public class Bar{ 
   
	public static final double BAR_SIGN;

	static { 
   
		// 这段程序会在类被加载时执行
		BAR_SIGN = (double)Math.random();
	} 
}

primitive类型的包装

由于object和primitive的差异,需要把primitive类型包装成object才能进入object体系。
泛型的规则,要求只能是类或者接口类,不能使primitive。
5.0之前,要手动,5.0之后,有autoboxing
可用在,参数、返回值、boolean判断、数值计算、赋值。
void takeNumber(Integer i){}可传入 int类型。
int giveNumber(){return Interger(0)}返回值可互相替代。
if (Boolean对象){}bool判断可用Boolean对象。
Integer i = Integer j + 3直接用于数值计算。
Interger i = 3赋值。

注:包装完了是个object引用,需要new出来

format

String.format ("%t", today)%t表示时间。

"%tc" // 完整时间
"%tr" // 只有时间
"%tA, %tB, %td" // 周月日
"%tA, %<tB %<td", today // <符号是个特殊的指示,用来告诉格式化程序重复利用之前用过的参数,不用写多个。

静态的import

import static java.lang.System.out;可以省略System
省略包名会有混淆,在golang中一般不提倡这么用。

内部类

class MyOuterClass{ 
   
	private int x;
	MyInner inner = new MyInner();
	class MyInner{ 
   
		void go(){ 
   
			x = 42; // 可以使用OuterClass内的所有
		}
	}
	public void doStuff(){ 
   
		// 从外部类以“内”的代码初始内部实例
		inner.go(); // 调用内部方法
	}
}

// 另一种,从外部类以“外”的代码初始内部实例
class Foo{ 
   
	public static void main(String[] args){ 
   
		MyOuterClass outObject = new MyOuterClass();
		MyOuterClass.MyInner innerObj = outObject.new MyInner();
		// 外部类.内部类 内部对象 = 外部对象.new 内部类构造函数; 
	}
}

注意点:

  1. 内部类的实例一定会绑在外部类的实例上。
  2. 内部类提供了在一个类中提供同一个接口实现多次的机会。
  3. 使用内部类的特征:独立、却又好像另一个类成员之一。
  4. 使用内部类代表外部类,外部类只能单继承。内部类可以实现多个接口,通过IS-A测试。

常用包、函数

String.format("...",...); // 格式化字符串
String doubleString = Double.toString(d); // double to string
import java.util.Calendar // 操作日期,除了Date里的,时间计算,roll, set,等等
import java.util.Date // 当前日期
Calendar cal = Calendar.getInstance(); // 静态方法返回一个
Calendar对象,不可直接 new Calendar();

java.util.Calendar

add(int field,int amount)
get(int field)
getInstance()
getTimeInMillis()
roll()
set()
set()
setTimeInMillis()

异常处理

public void takeRisk() throws BadException{ 
   
	if (abandonAllHope){ 
   
		throw new BadException();
	}
}

public void crossFingers(){ 
   
	try { 
   
		anObject.takeRisk();
	}catch (BadException ex){ 
   
		System.out.println("Aaargh!");
		ex.printStackTrace();
	}
}

throws 表示方法会抛出什么类型的错误。

RuntimeException被称为不检查异常,可以抛出和catch但是没有这个必要,编译器也不管。
任何继承过它的都会被编译器忽略。
try catch是处理真正的异常,而不是程序的逻辑错误。catch要做的是恢复的尝试,或者至少优雅的列出错误信息。

  • 可能会抛出异常的方法必须声明成 throws Exception。
  • 如果程序调用了会throws Exception的方法,那一定要try catch,告诉编译器注意到了
  • 如果不处理异常,还是可以正式地将异常ducking来通过编译

finally表示无论如何都要做的事,无论try,catch,都会进finally。如果try catch中有return,依然会执行finally

try{ 
   
} catch (AException ex) { 
   
} catch (BException ex){ 
   
}finally { 
   
}

Exception也是继承体系中的,可以多态,父类引用子类实例。
throws 父类,可以抛出它的所有子类。
catch父类,可以接它所有子类,但是不应该这么做。应该具体异常具体一个catch。
duck掉的意思是不处理exception,交给调用栈的上一层去处理。

// duck the exception
void foo() throws ClothingException { 
    // throws 出去,交给上层处理
	laundry.doLaundry()
}

异常处理规则:

  1. catch与finally不能没有try
  2. try一定要有catch或finally
  3. try与catch之间不能有程序
  4. 只带有finally的try必须要声明异常!!就是throws XXException对于try{}finally{}

序列化

把Object可以完整的保存下来,包括Object中的对其他Object的引用,序列化过程必须全部正确,如果有局部不正确,那整体也会出错。
Object -> ObjectOutputStream -> FileOutputStream -> 文件

  • 标记为transient可不被序列化。
  • 父类不序列化,子类可以标记为可序列化。
  • 如果两个对象引用了同一个对象,那么序列化时候也只会有一份(比较聪明)。

反序列化时。

  1. 对象从stream中读出来
  2. jvm通过存储信息判断出对象的class类型
  3. jvm尝试寻找和加载对象的类。如果jvm找不到,就会抛出exception。
  4. 新的对象会被放在堆上,不会调用构造函数。
  5. 如果对象在继承树上有个不可序列化的祖先类,则该不可序列化类以及之上的类的构造函数就会执行,一旦构造函数连锁启动后将无法停止。也就是说从第一个不可序列化的父类开始,全部都会重新初始状态。
  6. 对象的实例变量会被还原成序列化时的状态值。transient变量会被赋值null的对象,或者primitive的默认0值。

文件-> FileInputStream -> InputOutputStream -> Object
newFileInputStream(“xxx”)
newObjectInputStream(fs)
Obj = (Type)is.readObject()

读取对象的顺序必须与写入的顺序相同。

写入文件和写对象差不多。

try{ 
   
	FileWrite writer = new FileWriter("Foo.txt");
	writer.write("hello foo!");
	writer.close();
}catch ()...

File IO

import java.io.*;
class名java.io.File:
File表示磁盘上的文件,不是文件的内容。

File.mkdir(); // 建目录
File.isDirectory(); // 列出目录的内容
File.getAbsolutePath(); // 列出绝对路径
File.delete(); // 删除

BufferedXXX

缓冲区,可以和FileWriter连接,提高性能。
构造函数 : BufferedWriter(FileWriter)

要点

  • 用FileWriter这个连接串流来写入文本文件。
  • 将FileWriter连接到BufferedWriter可以提升效率。
  • File对象代表文件的路径,而不是文件本身。
  • 可以用File对象来创建、浏览、删除目录
  • 用到String文件名的串流,大部分都可以用File对象来代替String
  • 用FileReader来读取文本文件
  • 将FileReader链接到BufferedReader可以提升效率。

多线程

java.lang.Thread

  • java每个线程有独立的执行空间
  • java.lang.Thread的对象表示线程
  • Thread需要任务,任务实现Runnable接口
  • Runnable接口只有一个方法,就是void run()
  • run()是线程入口
  • Runnable作为Thread的构造函数参数,构建Thread
  • Thread在调用start之前处于新建立状态
  • start之后会建立出新的执行空间,被等待执行
  • java虚拟机有调度器
  • 线程会block
  • 不会保证调度时间合顺序

Thread.setName(String)可以帮线程取名字。用Thread.currentThread().getName()可以取出名字。

同步操作

synchronized修饰方法,使得它每次只能被单一的线程存取。
[外链图片转存中...(img-Uwt5RyyJ-1595669041476)]

集合与泛型

Collections Framewo 集合框架,能够支持绝大多数数据结构。

public static <T extends Comparable<? super T>> void sort(List<T> list)

public interface Comparable<T>
{ 
   
	int compareTo(Object b);
}

Interface Comparator<T>
{ 
   
	int	compare(T o1, T o2);
}

T extends Comparable
表示T必须继承/实现 Comparable,
? super T表示Comparable的类型参数必须是T或者T的父型。
extends在泛型里,可以是implements或者extends,即继承或者实现都可以。
sort也可以有两个参数的版本,第二个参数必须实现Comparator,比较两个Type。
Collection里面有三种,List,Set,Map。

Collection(itf)
	Set(itf)
		SortedSet(itf)
			TreeSet // 排序的Set
		LinkedHashSet // 带插入顺序的Set
		HashSet // 不带顺序的Set
	List(itf)
		ArrayList
		LikedList
		Vector

Map(itf)
	SortedMap(itf)
		TreeMap
	HashMap
	LinkedHashMap
	Hashtable

可以 HashSet .addAll(ArrayList), 一把全加进去。

对象的等价

引用相等性:
不同堆上的hashCode()不一样,内存不一样。可以用==比较。

对象相等性。
需要覆盖 hashCode()和equals()。

hashCode()先比较,不一样的,一定不一样,如果一样,那么一定是同一个对象。
在hash相同时,还不能确定对象一定想同,还需要用equals比较。

hashcode不一样是对象不一样的充分条件,hashcode不一样,equals为false是对象不一样的充分必要条件。

那么对象一样的 充分必要条件是 hashcode一样 或者 equals为true。

  1. 若equals被覆盖,那么hashCode一定也要被覆盖,否则比较没有意义,因为会先比较hashCode。foo.hashCode() == bar.hashCode() || foo.equals(bar)
  2. hashCode() 默认是取heap上的地址相关。
  3. equals()默认是执行==。如果equals没被覆盖,两个对象永远不会被视为相同的。
  4. equals()必须与hashcode == hashcode等值(推论)
	public class BookCompare implements Comparator<Book>{ 
   
			public int compare(Book a, Book b){ 
   
				return a.title.compareTo(b.title);
			}
	}

	BookCompare bCompare = new BookCompare()
	TreeSet<Book> tree = new TreeSet<Book>(bCompare);
	// 使用Comparator作为构造参数

也可以Book impletements Comparable来new TreeSet<Book>

总结:

  1. 要使用TreeSet,要么集合中的元素,实现Comparable接口
  2. 要么使用重装,取用Comparator参数的构造函数来创建TreeSet

HashMap<Key, Value> 用put和get。

泛型

父类数组可以接受子类数组作为入参。

public void takeAnimals(Animal[] animals);
{ 
   
	takeAnimals(dogs);// dog数组每个元素都继承Animal
}

但是ArrayList<Animal>不可以接受ArrayList<Dog>,会编译期失败。

对于数组来说会运行期失败。

泛型的万用字符。使用带有<?>的声明时,编译器不会让你加入任何东西到集合中!

// ? 继承或实现Animal的T
public void takeAnimals(ArrayList<? extends Animal> animals){ 
   
	for (Animal a : animals){ 
   
		a.eat(); // 不会报错
	}
	animals.add(new Cat()); // 会编译器报错
}

相同功能的另一种语法。

  1. 返回类型前声明。可以只声明一次。后面都用T。
    public <T extends Animal> void takeThing(ArrayList<T>) list)
  2. 用问号。每个地方都要声明 ?
    public void takeThing(ArrayList<? extends Animal>) list)

包,jar存档文件和部署

cd MyProject/source
javac -d ../classes

javac -d 可指定class的目录,一般时source+classes的形式。
从classes目录执行。

JAR:Java ARchive。是个pkzip格式文件,把一组类文件包装起来,只需交付一个JAR文件。

可执行的JAR代表用户不需要把文件抽出来就能运行。秘诀在于manifest文件,会带有JAR的信息,告诉JVM哪个类有main()方法。

创建可执行的JAR

  1. 确定所有类文件都在classes目录下
  2. 创建manifest.txt(中文:货单)来描述哪个类带有main()方法。其中内容为
    Main-Class MyApp 换行
  3. 使用jar -cvmf mainfest.txt app1.jar *.class打包成app1.jar

大部分完全在本机执行的java应用程序都是以可执行JAR来部署的。

执行JAR。

cd MyProject/classes
java -jar app1.jar

JVM要找到JAR,所以必须在classpath下,让jar曝光的最好方式就是放在工作目录下。

java -jar 告诉java虚拟机所给的是JAR,JVM检查JAR的Manifest寻找入口,没有入口就会发生运行时异常。

验证JAR打包的内容:
jar -tf packEx.jar-tf表示table file(tf)
META-INF/MANIFEST.MF是入口指示

$ jar -tf packEx.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/headfirstjava/
com/headfirstjava/MyDrawPanel.class
com/headfirstjava/PackageExercise.class

解压JAR打包的内容:
jar -xf packEx.jar-xf表示eXtract file
会把-tf的内容解压出来,并创建相应的目录。

把classes打包成包可以避免命名冲突。
类必须在完全对应于包结构的目录中才能包进包!
最好使用Domain作为前缀,这样不仅可以避免命名冲突,也可以显示一些额外的信息。
反向使用网址domain,这样只担心同公司的人的冲突。

import com.headfirstjava.projects.Chart

如上,com.headfirstjava是domain名称反过来。projects是工程名。类名Chart第一个字母是大写的。

编译package并运行试验:

  • Step1:
    目录结构:
    source/com/headfirstjava
    classes

  • Step2:
    在headfirstjava中写类的代码,并在首行加入 package com.headfirstjava;

  • Step3:
    在source目录下,执行 javac -d ../classes com/headfirstjava/*.java
    -d 后参数表示classes的输出目录,会自动建立对应的目录
    执行后目录结构:
    source/com/headfirstjava
    classes/com/headfirstjava

  • Step 4:
    cd 到 classes目录下,执行java com.headfirstjava.PackageExercise
    jvm会看得懂,并找寻当前目录下的com目录,其下应该有headfirstjava目录,那里应该能找到Class。class在其他位置都无法运行!

注意!一旦类被包进包中,就不能用简写名称调用,必须执行main()所属的完整类名,包括包结构。com/headfirstjava/类名 的每一层都要匹配上。

把package,com结构打包进jar

  • Step1:
    确定所有类文件在class目录下正确对应的包结构中。

  • Step2:
    创建manifest.txt,指定main class Main-Class: com.headfirstjava.PackageExercise,需要把com.xxx加上。

  • Step3:
    执行jar工具,创建带目录结构和manifest的JAR文件。
    jar -cvmf manifest.txt packEx.jar com,其中只需要com目录就够了,其下整个包的类都会被包进JAR。

总结:
编译:source目录下,javac javac -d ../classes
执行:classes目录下,java com.xx.Class
打包:classes目录下,新建manifest, jar -cvmf manifest.txt packEx.jar com
执行:classes目录下,java -jar packEx.jar

Addtional

不变性

String具有不变性。
创建新的String时,JVM会把它放到StringPool中,如果有相同的String,JVM不会重复创建,只会引用。String不可修改,所以在for循环中建立10个String,有9个是在浪费空间。

包装类有不变性。
Integer两个用途。1. primitive主类型包装成对象。2. 使用静态的工具方法parseInt()。
Integer iWrap = new Integer(42);
它的值永远是42,没setter方法。

如何节省内存?使用StringBuilder!

断言

没打开断言时,JVM会忽略。使用断言替代println()。
使用方法:

assert(height>0); // false,抛出AssertionError
assert(height>0): "height = "+height+"weight = "+weight;

冒号后面的指令,可以是任何解出非null值的合法Java语句,千万不要在assert中改变对象状态!不然打开assertion执行时可能会改变程序的行为。

编译没有变化。
执行时 java -ea TestDriveGameEnable Assertion

Anonymous和Static Nested Classes

匿名和静态嵌套类。

静态嵌套类

public class FooOuter{ 
   
	// 静态的嵌套类
	static class BarInner{ 
   
		void sayIt(){ 
   
			System.out.println("method of a static inner class");
		}
	}
}

class Test{ 
   
	public static void main(String[] args){ 
   
		// 因为是static的,所以不需要外层实例,只需要外层类名
		FooOuter.BarInner foo = new FooOuter.BarInner();
		foo.sayIt();
	}
}

静态嵌套像一般非嵌套,他们未与外层对象产生特殊关联。但因为还是外层的一个成员,所以能够存取任何外层的static的私有成员。

nested和inner的差别

除了内嵌的类,还可以匿名类直接创建对象,就在对象new出来的地方把类定义了。这个类称为匿名类。
例如:直接以interfaceActionListener占位“类名”的地方new出来一个对象。

public static void main(){ 
   
	JButton button = new JButtion();
	button.addActionListener(new ActionListener(){ 
   
		public void actionPerformed(ActionEvent ev){ 
   
			System.exit(0);
		}
	});
}

存取权限和存取修饰符(谁可以看到什么)

public:完全开放
private:只对类内开放
protected:对子类及包内开放,只能用在继承上。
(啥修饰符都不加即default)default:对包内开放。同一包内default。

String和StringBuffer、StringBuilder的方法

三者通用的方法:
char charAt(int index); // index位置的字符
int length(); // 字符长度
String subString(int start, int end); // 字串
String toString(); // Object的String表示值

连接字符串:
String concat(string); // String使用
String append(String); // StringBxxxx使用

only String的方法:
String replace(char old, char new); // 替换
String subString(int begin, int end); // 字串
char[] toCharArray(); // 转char数组
String toLowerCase(); // 
String toUpperCase(); // 
String trim(); // 删后面空格
String valueOf(char[]); // 转成String
String valueOf(int i); // 转成String,其他primitive主数据亦可

only StringBuffer or StringBuilder的方法:
StringBxxxx delete(int start,int end);
StringBxxxx insert(int offset, any primitive or a char[]);
StringBxxxx replace(int start, int end, String s);
StringBxxxx reverse(); // 反转
void setCharAt(int index, char ch); // 替换

多维数组

数组也是对象。

int[][] a2d = new int[4][2]; // 二维数组
实际上是5个数组,一个int[4][],和4int[2]。
操作数组:
1) 存取第三个数组的第二个元素:int x = a2d[2][1];
2) 对某个子数组创建引用: int[] copy = a2d[1];
3) 初始化2*3数组:int[][] x = { 
   { 
   2,3,4},{ 
   7, 8, 9}};
4) 创建非常规二维数组:
	int[][] y = new int[2][]; // 长度2的第一层
	y[0] = new int[3]; // 3个
	y[1] = new int[5]; // 5

枚举

枚举类型 Enum

修饰符 enum关键字 类名 { 
   枚举名1, 枚举名2, ...};
public enum Members { 
   A, B, C};
public Members selectedBandMember;
selectedBandMember只能是A, B, C中的一个
enum会继承java.lang.Enum,创建enum时,其实是隐含地继承java.lang.Enum来创建新的类。
可以使用==.equals(),或者switch-case中。

可以在enum中加入构造函数、方法、变量和特定常量内容(class body),不常见,但是可行。因为 Enum实际上是继承java.lang.Enum类,是个final类。,编译器会为我们添加静态的values()方法,所以可以用 Members.values() 返回 Members[]。
编译器会添加valueOf(String s)方法。静态初始static{实例化枚举实例}。

public class HfjEnum{ 
   
	enum Names{ 
   
		// 枚举名(构造函数参数)
		JERRY("lead guitar"){ 
   
			// 为每个实例提供特定的行为
			@Override // 复写
			public String sings(){ 
   
				return "plaintively";
			}
			@Override
		    <T> T doAction(T t) { 
   
		        //your implementation
		    }
		},
		BOBBY("rhythm guitar"){ 
   
			@Override // 复写
			public String sings(){ 
   
				return "hoarsely";
			}
			@Override
		    <T> T doAction(T t) { 
   
		        //your implementation
		    }
		},
		PHIL("bass"){ 
   
			@Override // 复写抽象函数
		    <T> T doAction(T t) { 
   
		        //your implementation
		    }
		}; // 分号结尾

		private String instrument; // Enum的私有变量
		Names(String instrument){ 
   
			// enum的构造函数,传入参数见上面JERRY,BOBBY括号内的。
		}
		public String getInstrument(){ 
    // enum的方法
			return this.instrument; // 返回私有变量
		}
		public String sings(){ 
   
			return "occasionally";
		}
		abstract<T> T doAction (T t); // 可以用泛型
	}
}
String string = Names.JERRY.<String>doAction("hello"); // 可以这么调用
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/156415.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • linux 查看内存大小命令,Linux查看命令:CPU型号,内存大小,硬盘空间「建议收藏」

    linux 查看内存大小命令,Linux查看命令:CPU型号,内存大小,硬盘空间「建议收藏」#cat/proc/cpuinfo|grep”physicalid”|uniq|wc-l说明:uniq命令:删除重复行;wc–l命令:统计行数1.2查看CPU核数#cat/proc/cpuinfo|grep”cpucores”|uniqcpucores:4说明:cpu核数为41.3查看CPU型号#cat/proc/cpuinfo|grep’mo…

  • webservice传递特殊字符时的解决的方法

    webservice传递特殊字符时的解决的方法

    2021年11月29日
  • 页面左侧二级菜单20种案例「建议收藏」

    页面左侧二级菜单20种案例「建议收藏」 本文由码农网&amp;nbsp;–小峰原创,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!jQuery作为一款主流的JavaScript前端开发框架,深受广告开发者的亲睐,同时jQuery有着不计其数的插件,特别是菜单插件更为丰富,本文将要为大家介绍20个绚丽而实用的jQuery侧边栏菜单,这些侧边栏菜单可以用在不同风格的网页上,如…

  • 1024是程序员的什么节日(重阳节的时候干什么)

    1024程序员节1024程序员节是广大程序员的共同节日。1024是2的十次方,二进制计数的基本计量单位之一。针对程序员经常周末加班与工作日熬夜的情况,部分互联网机构倡议每年的10月24日为1024程序员节,在这一天建议程序员拒绝加班。程序员就像是一个个1024,以最低调、踏实、核心的功能模块搭建起这个科技世界。1G=1024M,而1G与1级谐音,也有一级棒的意思。1、节日背景程序员(英文Programmer)是从事前端、后端程序开发、系统运维、测试等的专业人员。一般将程序员分为程序设计人员和程序.

  • Python中的字符串切片(截取字符串)

                            字符串索引示意图字符串切片也就是截取字符串,取子串Python中字符串切片方法字符串[开始索引:结束索引:步长]切取字符串为开始索引到结束索引-1内的字符串步长不指定时步长为1字符串[开始索引:结束索引]练习样例#1.截取2-5位置的字符num_str_1=num_str[2:6]print(num_…

  • 解释型语言与编译型语言的区别?_编译型语言和解释型语言的优缺点

    解释型语言与编译型语言的区别?_编译型语言和解释型语言的优缺点编译型语言在程序执行之前,有一个单独的编译过程,将程序翻译成机器语言,以后执行这个程序的时候,就不用再进行翻译了。解释型语言,是在运行的时候将程序翻译成机器语言,所以运行速度相对于编译型语言要慢。C/

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号