Java源码分析-String类分析 javaspring源码
yuyutoo 2024-10-21 12:03 2 浏览 0 评论
文/小图灵视界
本头条号将会继续分析JDK8的源码,欢迎关注和收藏,也会将分析笔记开源。
位置:java.lang
String类是除了Object类外,最基础的类,最重要的类,在开发过程中使用最常用的类,也是使用最多的类。String类的定义如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
String类由final修饰符修饰,是不可被子类继承的类,是不可改变的类。String类实现了Serializable、Comparable、CharSequence三个接口,实现Serializable接口,对象就可以进行序列化,实现Comparable接口,String类的对象之间就可以进行比较,CharSequence接口提供了如下方法:
//计算长度
int length();
//返回某个位置的字符
char charAt(int index);
//返回从start到end位置的子CharSequence
CharSequence subSequence(int start, int end);
//输出的字符串
public String toString();
String实现CharSequence的上述所有接口方法,为String提供计算长度、定位字符串某个位置的字符、返回字符串的子字符串以及标准的输出字符。String类有如下几个重要的属性:
//保存字符串
private final char value[];
//缓存字符串的哈希码
private int hash;
//用于指定哪些字段需要被默认序列化
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
String类底层本质是用char数组进行保存字符的,char数组value属性用来保存字符串,hash属性是哈希码的值,默认值为0,serialPersistentFields属性用于指定哪些字段需要被默认序列化。这些属性都是final修饰,不可改变,String类也没有提供获取和修改这些属性的方法。
常用的构造方法
//默认为空字符串
public String() {
this.value = "".value;
}
//用字符创构建字符串
public String(String original) {
this.value = original.value;
this.hash = original.hash
}
//用char数组初始化
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//用StringBuffer初始化
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
//用StringBuilder初始化
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
String类是不可变的类,在初始化的过程中都是采用副本进行赋值。String() 构造器是默认的构造器,用空字符串的副本进行初始化( this.value = "".value;),String(String original)构造器将字符串original的value和hash属性的副本赋值给新的字符串的value和hash属性。String(char value[])将char 数组的复制给this.value,是用 Arrays.copyOf方法复制的, Arrays.copyOf方法底层是新创建一个char数组,然后利用本地方法System.arraycopy将value数组的值复制给新创建char数组。
String(StringBuffer buffer) 构造函数将StringBuffer的char数组复制给字符串,这个构造器用synchronized对buffer加锁,原因是String类是不可变的,在多线程的环境下是安全的,但是StringBuffer类是不安全的,为了保证用StringBuffer初始化字符串时的安全性而加锁。最后一个常用的构造方法是String(StringBuilder builder),利用StringBuilder进行初始化,将StringBuilder的char数组复制给字符串,StringBuilder类是安全的。
用的方法
public int length() {
return value.length;
}
length()方法返回字符串的长度,这个长度就是底层char数组的长度。
public boolean isEmpty() {
return value.length == 0;
}
isEmpty()方法判断字符串的长度是否等于0,实际判断的是底层char数组的长度是否等于0。这个方法具有误导性,实际上并不是判空操作,在开发过程中判断一个字符串是否为空,常用if(str==null || str.length()==0)进行判空操作。
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
charAt(int index) 方法返回给定位置index的字符,首先判断给定位置的index是否合法,如果合法,返回char数组value的index位置的字符,否则,返回StringIndexOutOfBoundsException异常。
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
getChars(char dst[], int dstBegin)方法将字符串的value数组从dst数组的dstBegin位置复制到dst数组中。利用了 System.arraycopy本地方法复制。
public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
Objects.requireNonNull(dst);
int j = dstBegin;
int n = srcEnd;
int i = srcBegin;
//重新复制一个char数组
char[] val = value; /* avoid getfield opcode */
while (i < n) {
//将char强转为byte
dst[j++] = (byte)val[i++];
}
}
getBytes方法将字符串的char数组复制给byte数组。srcBegin参数表示的从value数组的srcBegin位置,srcEnd参数表示的从value数组的srcEnd位置,复制srcBegin位置到srcEnd位置的字符。这个方法首先判断参数是否越界,getBytes方法并不是直接操作字符串中的chat数组value,而是复制了一个value的副本val,原因是避免getfield操作,最后用循环遍历副本val各个位置的值转为byte类型赋值给dst数组。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String类重写了Object父类的equals方法,首先判断比较的对象anObject是否是这个对象this,如果是直接返回true,否则继续判断这个对象是否属于String类型,如果是的话,判断下anObject对象的value数组的长度是否等于这个字符串的value数组的长度,如果是的话,遍历两个比较对象的value数组中的每个位置的字符是否相等,如果相等返回true,否则就返回false。
equals方法比较巧妙,先判断是否是this和判断两个比较字符之间长度,这两个判断提高了快速判断两个字符串是否相等,只有这两个判断都成立了,才遍历比较两个字符相同位置的字符是否相等。
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}
contentEquals方法是判断字符序列(CharSequence)的内容与字符串的内容是否相等。传入的参数是CharSequence,首先判断传入的参数是否是AbstractStringBuilder类型,AbstractStringBuilder类型是StringBuffer类和StringBuilder类的父类,如果属于AbstractStringBuilder类型,则判断是否属于StringBuffer类型,如果是则用synchronized加锁调用nonSyncContentEquals方法,如果属于StringBuilder类则不加锁调用nonSyncContentEquals方法。如果参数属于String类型,则调用equals方法,如果属于一般的CharSequence类型,则判断字符串的长度与比较的字符序列的长度是否相等,如果不相等返回false,否则继续遍历比较两个比较对象底层char数组的每个位置的字符是否相等。最后再来分析下nonSyncContentEquals方法,nonSyncContentEquals方法如下:
private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
char v1[] = value;
char v2[] = sb.getValue();
int n = v1.length;
if (n != sb.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != v2[i]) {
return false;
}
}
return true;
}
nonSyncContentEquals方法就是判断两个比较对象底层的char数组之间的相同位置的字符是否相等。首先判断比较序列之间的长度是否相等,然后遍历两个对象底层数组相同位置的字符是否相等。
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
compareTo方法比较两个字符串的大小,首先获取两个字符串的长度,然后获得两个字符串的长度中较小的长度lim,遍历比较两个字符串小于lim范围里字符的大小,当两个字符串相同位置的字符不相等时,返回两个字符相减的结果,如果较小长度lim范围里,两个字符串的字符都相等,那么比较两个字符串的长度大小。
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
//指定toffset参数为0,说明字符串是否包含指定的前缀prefix,索引从0开始。
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
//从value.length - suffix.value.length索引开始的子字符串是否包含指定的后缀suffix
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
startsWith方法是从指定索引开始的字符串的子字符串是否包含指定的前缀。首先进行边界的判断,参数toffset是否小于0,以及toffset是否大于字符串的长度减去指定前缀的长度,如果toffset小于0或者toffset大于字符串的长度减去指定前缀的长度,则返回false,否则,遍历指定前缀与指定索引开始的字符串的子字符串的字符是否都相等,否则返回false。如果都不满足上述的条件,返回true。
startsWith(String prefix)方法和endsWith(String suffix) 都是在startsWith(String prefix, int toffset)方法的基础上来的,只是toffset的值不一样,当toffset等于0时,就变成了startsWith(String prefix)方法;当toffset等于value.length - suffix.value.length时,就变成了endsWith(String suffix)方法。
public int indexOf(int ch, int fromIndex) {
//边界判断
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
//当ch<65535,一个字节8位,两个字节16位,2的16次方等于65535
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
在讲解indexOf(int ch, int fromIndex) 方法时,先了解下Unicode 编码,Unicode 编码是为了解决能够使计算机实现跨语言、跨平台的文本转换及处理。可以容纳世界上所有文字和符号的字符编码方案。本来想讲解下Unicode ,但是发现网上已经通俗易懂讲解Unicode 编码的文章,所以就不展开来讲了,感兴趣的可以参考如下博客:
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
indexOf(int ch, int fromIndex) 方法首先进行边界判断,如果输入的参数字符ch小于65535,就直接遍历String类型的char数组value,参数小于65535,说明该字符由两个字节组成。当value数组中的字符与参数ch相等,就返回该位置的索引。如果输入的参数字符大于等于65535,就调用indexOfSupplementary(int ch, int fromIndex) 方法,该方法如下:
private int indexOfSupplementary(int ch, int fromIndex) {
//判断是否是合法的Unicode 对应的码点
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
final char hi = Character.highSurrogate(ch);
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
当输入参数大于等于两个字节,即大于等于65535时,调用indexOfSupplementary(int ch, int fromIndex) 方法,当输入的参数字符ch是合法的码点,获取输入参数ch的大端字符和小端字符,比较ch的大端字符和小端字符与value数组第i个位置和第i+1个位置的值是否相等,相等就返回索引i。如果没有找到相等的值,就返回-1。
public int lastIndexOf(int ch, int fromIndex) {
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
int i = Math.min(fromIndex, value.length - 1);
for (; i >= 0; i--) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return lastIndexOfSupplementary(ch, fromIndex);
}
}
private int lastIndexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
char hi = Character.highSurrogate(ch);
char lo = Character.lowSurrogate(ch);
int i = Math.min(fromIndex, value.length - 2);
for (; i >= 0; i--) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
lastIndexOf(int ch, int fromIndex)方法与indexOf(int ch, int fromIndex) 方法逻辑基本一样,只是两者在遍历数组value的顺序不一样,lastIndexOf(int ch, int fromIndex)是从数组后面往前面遍历,indexOf(int ch, int fromIndex) 是从数组前面往后面遍历。
indexOf和lastIndexOf方法都很多其他参数的重载方法,都是在indexOf(int ch, int fromIndex) 方法和lastIndexOf(int ch, int fromIndex)方法的基础上拓展而来的,只要理解这两个方法,其他重载的方法都比较简单,这里就不分析了。
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
substring(int beginIndex)方法返回从beginIndex位置开始的子字符串,首先判断beginIndex是否合法,当beginIndex小于0或者子字符串的小于0时(value.length - beginIndex),抛出StringIndexOutOfBoundsException异常。当beginIndex等于0时,直接返回this,返回字符串是原来的字符串,否则创建新的String对象返回。
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
substring(int beginIndex, int endIndex) 方法是返回从beginIndex开始到endIndex位置结束的子字符串。substring(int beginIndex, int endIndex) 首先判断beginIndex和endIndex的合法性,当beginIndex和endIndex不合法时,抛出StringIndexOutOfBoundsException异常。beginIndex等于0并且endIndex 等于 value.length,返回this(原字符串),否则重新创建一个String对象返回。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
concat(String str)方法的作用是拼接两个字符串,当拼接的字符串str的长度等于0,直接返回原字符串。否则通过 Arrays.copyOf将value数组复制到长度为len + otherLen的字符数组buf中,然后将字符串复制到buf数组中,最后用buf数组创建新的字符串返回。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
//先找到字符串中与oldChar相等的值的位置i
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
//如果找到字符串中与oldChar相等的值的位置i
if (i < len) {
char buf[] = new char[len];
//将小于i位置的字符保存在buf中
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
//将所有等于oldChar值的替换为newChar,否则等于原来的字符串
while (i < len) {
char c = val[i];
//如果i位置的
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//返回新创建的String对象
return new String(buf, true);
}
}
return this;
}
replace(char oldChar, char newChar)方法的作用是将字符串中老字符oldChar替换为新字符newChar,如果oldChar等于newChar,直接返回原来的字符串,否则先找到字符串中与oldChar相等的第一个字符的位置i。如果找到字符串中与oldChar相等的第一个字符的位置i,将用两步来替换旧的值,第一步将小于i位置的字符保存在buf,第二部将所有等于oldChar值的替换为newChar,如果不等于oldChar将原来的字符串保存在buf,最后通过buf创建新的String对象。
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
//从前面找到不等于空字符' '的第一个位置
while ((st < len) && (val[st] <= ' ')) {
st++;
}
//从后面找到不等于空字符' '的第一个位置
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果原来字符串前面和后面存在空字符串' ',返回去除空字符串' '的子字符串
//否则,返回原来的字符串
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
trim() 方法是去除字符串两边的空字符‘ ’, trim()方法从字符串的前面和后面分别遍历字符,寻找不等于空字符‘ ’的位置,如果原来字符串前面和后面存在空字符串' ',返回去除空字符串' '的子字符串,否则,返回原来的字符串。
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
toCharArray()方法返回字符串的char数组,通过本地方法 System.arraycopy复制字符串的字符到char数组中。该方法中有一段注释说明不能使用Arrays.copyOf复制字符到char数组中,是因为类初始化的顺序问题。这注释也没有很清楚解释为什么toCharArray()中不能用Arrays.copyOf复制字符。经过查找资料,找到一段清楚的解释:
引用:https://my.oschina.net/u/3268478/blog/3011267
虽然String 和Arrays 都属于rt.jar中的类,但是BootstrapClassloader 在加载这两个类的顺序是不同的。所以当String.class被加载进内存的时候,Arrays此时没有被加载,所以直接使用肯定会抛异常。而System.arrayCopy是使用native代码,则不会有这个问题。
public native String intern();
intern()是本地方法,当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。对于两个字符串s和t,如果s.intern() == t.intern()为true,则s.equals(t)为true。intern()方法在JVM的函数为Java_java_lang_String_intern:
JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
Java_java_lang_String_intern函数返回调用JVM_InternString函数的结果。JVM_InternString函数的方法为:
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollector oam;
//如果java中的字符串等于null,直接返回null
if (str == NULL) return NULL;
//将java的字符串jstring转为c++中的普通对象oop
oop string = JNIHandles::resolve_non_null(str);
//从字符常量池StringTable中查找字符串,
oop result = StringTable::intern(string, CHECK_NULL);
//将从字符常量池StringTable中查找字符串的转换成java的字符串
return (jstring) JNIHandles::make_local(env, result);
JVM_END
JVM_InternString函数从StringTable常量池中寻找字符串,StringTable相等于java中hashTable表,如果不存在这个字符串,那么就将这个字符串的引用保存在StringTable常量池中,如果存在这个字符串,就返回这个字符串的引用。StringTable常量池中的字符串越来越多的时候,查找效率会越来越低,所有也不能大量的使用intern(),否则导致性能下降。
相关推荐
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
-
Mysql和Oracle实现序列自增/*ORACLE设置自增序列oracle本身不支持如mysql的AUTO_INCREMENT自增方式,我们可以用序列加触发器的形式实现,假如有一个表T_WORKM...
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
-
概述今天主要简单介绍一下Oracle12c的一些新特性,仅供参考。参考:http://docs.oracle.com/database/121/NEWFT/chapter12102.htm#NEWFT...
- MySQL CREATE TABLE 简单设计模板交流
-
推荐用MySQL8.0(2018/4/19发布,开发者说同比5.7快2倍)或同类型以上版本....
- mysql学习9:创建数据库(mysql5.5创建数据库)
-
前言:我也是在学习过程中,不对的地方请谅解showdatabases;#查看数据库表createdatabasename...
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
-
执行"CREATETABLE新表ASSELECT*FROM原表;"后,新表与原表的字段一致,但主键、索引不会复制到新表,会把原表的表记录复制到新表。...
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
-
在街上看到的PandaDunk的超载可能让一些球鞋迷们望而却步,但Dunk的浪潮仍然强劲,看不到尽头。我们看到的很多版本都是为女性和儿童制作的,这种新配色为后者引入了一种令人耳目一新的新选择,而...
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
-
多功能雷达AN/SPY-1的特性和技术能力,该雷达已经在美国海军服役了30多年,其修改-AN/SPY-1A、AN/SPY-1B(V)、AN/SPY-1D、AN/SPY-1D(V),以及雷神...
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
-
全面分析汽车音响使用或安装技术常识一:主机是大多数人最熟习的音响器材,有关主机的各种性能及规格,也是耳熟能详的事,以下是一些在使用或安装时,比较需要注意的事项:LOUDNESS:几年前的主机,此按...
- 【推荐】ProAc Response系列扬声器逐个看
-
有考牌(公认好声音)扬声器之称ProAcTablette小音箱,相信不少音响发烧友都曾经,或者现在依然持有,正当大家逐渐掌握Tablette的摆位设定与器材配搭之后,下一步就会考虑升级至表现更全...
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
-
作者:初吻给了烟sco混迹张大妈时日不短了,手没少剁。家里有了汪星人,吸尘器使用频率相当高,偶尔零星打扫用卧式的实在麻烦(汪星人:你这分明是找借口,我掉毛是满屋子都有,铲屎君都是用卧式满屋子吸的,你...
- 专题|一个品牌一件产品(英国篇)之Quested(罗杰之声)
-
Quested(罗杰之声)代表产品:Q212FS品牌介绍Quested(罗杰之声)是录音监听领域的传奇品牌,由英国录音师RogerQuested于1985年创立。在成立Quested之前,Roger...
- 常用半导体中英对照表(建议收藏)(半导体英文术语)
-
作为一个源自国外的技术,半导体产业涉及许多英文术语。加之从业者很多都有海外经历或习惯于用英文表达相关技术和工艺节点,这就导致许多英文术语翻译成中文后,仍有不少人照应不上或不知如何翻译。为此,我们整理了...
- Fyne Audio F502SP 2.5音路低音反射式落地音箱评测
-
FyneAudio的F500系列,有新成员了!不过,新成员不是新的款式,却是根据原有款式提出特别版。特别版产品在原有型号后标注了SP字样,意思是SpecialProduction。Fyne一共推出...
- 有哪些免费的内存数据库(In-Memory Database)
-
以下是一些常见的免费的内存数据库:1.Redis:Redis是一个开源的内存数据库,它支持多种数据结构,如字符串、哈希表、列表、集合和有序集合。Redis提供了快速的读写操作,并且支持持久化数据到磁...
- RazorSQL Mac版(SQL数据库查询工具)
-
RazorSQLMac特别版是一款看似简单实则功能非常出色的SQL数据库查询、编辑、浏览和管理工具。RazorSQLformac特别版可以帮你管理多个数据库,支持主流的30多种数据库,包括Ca...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
推荐7个模板代码和其他游戏源码下载的网址
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
- MySQL CREATE TABLE 简单设计模板交流
- mysql学习9:创建数据库(mysql5.5创建数据库)
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
- 【推荐】ProAc Response系列扬声器逐个看
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)