和小伙伴们仔细梳理一下 Spring 国际化吧!从用法到源码!

国际化(Internationalization,简称 I18N)是指在 Java 应用程序中实现国际化的技术和方法。Java 提供了一套强大的国际化支持,使开发人员能够编写适应不同语言、地区和文化的应用程序。

Java 国际化的主要目标是使应用程序能够在不同语言环境下运行,并提供相应的本地化体验。以下是 Java 国际化的一些关键概念和组件:

  1. ResourceBundle:ResourceBundle 是 Java 国际化的核心组件之一,用于存储本地化的文本和其他资源。它根据当前的 Locale(区域设置)加载相应的资源文件,以提供与用户语言和地区相匹配的内容。
  2. Locale:Locale 表示特定的语言和地区。Java 中的 Locale 对象包含了语言、国家/地区和可选的变体信息。通过使用 Locale,可以确定应用程序应该使用哪种语言和地区的本地化资源。
  3. MessageFormat:MessageFormat 是 Java 提供的一种格式化消息的工具类。它允许开发人员根据不同的语言和地区,将占位符替换为相应的值,并进行灵活的消息格式化。
  4. DateFormat 和 NumberFormat:Java 提供了 DateFormat 和 NumberFormat 类,用于在不同的语言和地区格式化日期、时间和数字。这些类可以根据 Locale 的不同,自动适应不同的语言和地区的格式规则。
  5. Properties 文件:Properties 文件是一种常见的配置文件格式,用于存储键值对。在 Java 国际化中,可以使用 Properties 文件来存储本地化文本和其他资源的键值对。

通过使用 Java 国际化的技术和组件,开发人员可以轻松地为 Java 应用程序提供多语言支持。应用程序可以根据用户的 Locale 加载相应的资源,并根据不同的语言和地区提供本地化的用户界面、日期时间格式、数字格式等。这样,应用程序就能够更好地适应全球用户的需求,提供更好的用户体验。

1. Java 国际化

经过前面的介绍,小伙伴们已经了解到,Java 本身实际上已经提供了一整套的国际化方案,Spring 中当然也有国际化,Spring 中的国际化实际上就是对 Java 国际化的二次封装。

所以我们先来了解下 Java 中的国际化怎么玩。

1.1 基本用法

首先我们需要定义自己的资源文件,资源文件命名方式是:

  • 资源名_语言名称_国家/地区名称.properties

其中 _语言名称_国家/地区名称 可以省略,如果省略的话,这个文件将作为默认的资源文件。

现在假设我在 resources 目录下创建如下三个资源文件:

三个资源文件的内容分别如下。

content.properties:

hello=默认内容

content_en_US.properties:

hello=hello world

content_zh_CN.properties:

hello=你好世界!

接下来我们看下 Java 代码如何加载。

Locale localeEn = new Locale("en", "US");
Locale localeZh = new Locale("zh", "CN");
ResourceBundle res = ResourceBundle.getBundle("content", localeZh);
String hello = res.getString("hello");
System.out.println("hello = " + hello);

首先我们先来定义 Locale 对象,这个 Locale 对象相当于定义本地环境,说明自己当前的语言环境和地区信息,然后调用 ResourceBundle.getBundle 方法去加载配置文件,该方法第一个参数就是资源的名称,第二个参数则是当前的环境,加载完成之后,就可以从 res 变量中提取出来数据了。而且这个提取是根据当前系统环境提取的。

在上面的案例中,如果配置的 locale 实际上并不存在,那么就会读取 content.properties 文件中的内容(相当于这就是默认的配置)。

1.2 Format

Java 中的国际化还提供了一些 Format 对象,用来格式化传入的资源。

Format 主要有三类,分别是:

  1. MessageFormat:这个是字符串格式化,可以在资源中配置一些占位符,在提取的时候再将这些占位符进行填充。
  2. DateFormat:这个是日期的格式化。
  3. NumberFormat:这个是数字的格式化。

不过这三个完全可以单独当成工具类来使用,并非总是要结合 I18N 一起来用,实际上我们在日常的开发中,就会经常使用 DateFormat 的子类 SimpleDateFormat。

这里我把三个分别举个例子给大家演示下。

MessageFormat

对于这种,我们在定义资源的时候,可以使用占位符,例如下面这样:

hello=你好世界!
name=你好 {0},欢迎来到 {1}

那么这里 {0}{1} 就是占位符,将来读取到这个字符串之后,可以给占位符的位置填充数据。

Locale localeEn = new Locale("en", "US");
Locale localeZh = new Locale("zh", "CN");
ResourceBundle res = ResourceBundle.getBundle("content", localeZh);
MessageFormat format = new MessageFormat(res.getString("name"));
Object[] arguments = new Object[]{"javaboy", "Spring源码学习课程"};
String s = format.format(arguments);
System.out.println("s = " + s);

那么最终打印结果如下:

DateFormat

这个是根据当前环境信息对日期进行格式化,中文的就格式化为中文日期,英文就格式化为英文日期:

Date date = new Date();
DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, new Locale("zh", "CN"));
DateFormat df2 = DateFormat.getDateInstance(DateFormat.LONG, new Locale("en", "US"));
System.out.println(df.format(date));
System.out.println(df2.format(date));

参数 LONG 表示演示完整的日期信息。

执行结果如下:

NumberFormat

数字格式化这块比较典型的就是关于货币的格式化了,我们来看个例子:

Locale localeEn = new Locale("en", "US");
Locale localeZh = new Locale("zh", "CN");
NumberFormat formatZh = NumberFormat.getCurrencyInstance(localeZh);
NumberFormat formatEn = NumberFormat.getCurrencyInstance(localeEn);
double num = 199.99;
System.out.println("formatZh.format(num) = " + formatZh.format(num));
System.out.println("formatEn.format(num) = " + formatEn.format(num));

根据不同的 Locale 来获取不同的货币格式化实例。

最终打印结果如下:

Java 中提供的国际化,差不多就这么玩!

2. Spring 国际化

Spring 的国际化,实际上就是在 Java 国际化的基础之上做了一些封装,提供了一些新的能力。

2.1 实践

先来一个简单的案例来看看 Spring 中的国际化怎么使用。

首先我们的资源文件跟前面第一小节的一致,不再赘述。

Spring 中需要我们首先提供一个 MessageSource 实例,常用的 MessageSource 实例是 ReloadableResourceBundleMessageSource,这是一个具备自动刷新能力的 MessageSource,即,用户修改了配置文件之后,在项目不重启的情况下,新的配置就能生效。

配置方式很简答,我们只需要将这个 Bean 注册到 Spring 容器中:

@Bean
ReloadableResourceBundleMessageSource messageSource() {
    ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
    source.setBasenames("content");
    return source;
}

这个 Bean 在注册的时候,有一个固定要求:beanName 必须是 messageSource。为什么是这样,等松哥一会分析源码的时候大家就看明白了。为 bean 设置 basename,也就是配置文件的基础名称。

接下来我们就可以使用了:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
MessageSource source = ctx.getBean(MessageSource.class);
Locale localeZh = new Locale("zh", "CN");
String hello = source.getMessage("hello", null, localeZh);
System.out.println("hello = " + hello);
Object[] params = new Object[]{"javaboy","world"};
String name = source.getMessage("name", params, localeZh);
System.out.println("name = " + name);

当然,一般在应用中,我们会对获取资源文件内容的方法进行封装再用。

封装类似下面这样:

@Component
public class MessageUtils implements MessageSourceAware {
    private static MessageSource messageSource;
    private static Locale currentLocale = new Locale("zh","CN");

    public static Locale getCurrentLocale() {
        return currentLocale;
    }

    public static void setCurrentLocale(Locale currentLocale) {
        MessageUtils.currentLocale = currentLocale;
    }

    public static String getMessage(String key) {
        return messageSource.getMessage(key, null, key, currentLocale);
    }

    public static String getMessage(String key, Locale locale) {
        return messageSource.getMessage(key, null, key, locale == null ? currentLocale : locale);
    }

    public static String getMessage(String key, String defaultMessage) {
        return messageSource.getMessage(key, null, defaultMessage == null ? key : defaultMessage, currentLocale);
    }

    public static String getMessage(String key, String defaultMessage, Locale locale) {
        return messageSource.getMessage(key, null, defaultMessage == null ? key : defaultMessage, locale == null ? currentLocale : locale);
    }

    public static String getMessage(String key, Object[] placeHolders) {
        return messageSource.getMessage(key, placeHolders, key, currentLocale);
    }

    public static String getMessage(String key, Object[] placeHolders, String defaultMessage) {
        return messageSource.getMessage(key, placeHolders, defaultMessage == null ? key : defaultMessage, currentLocale);
    }

    public static String getMessage(String key, Object[] placeHolders, Locale locale) {
        return messageSource.getMessage(key, placeHolders, key, locale == null ? currentLocale : locale);
    }

    public static String getMessage(String key, Object[] placeHolders, String defaultMessage, Locale locale) {
        return messageSource.getMessage(key, placeHolders, defaultMessage == null ? key : defaultMessage, locale == null ? currentLocale : locale);
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    
}

这个工具类实现了 MessageSourceAware 接口,这样就可以拿到 messageSource 对象,然后将 getMessage 方法进行封装。

用法其实并不难。

2.2 原理分析

再来看原理分析。

首先,在之前的分析中,小伙伴们知道,Spring 容器在初始化的时候,都会调用到 AbstractApplicationContext#refresh 方法,这个方法内部又调用了 initMessageSource 方法,没错,这个方法就是用来初始化 MessageSource 的,我们来看下这个方法的逻辑:

protected void initMessageSource() {
	ConfigurableListableBeanFactory beanFactory = getBeanFactory();
	if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
		this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
		// Make MessageSource aware of parent MessageSource.
		if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource hms &&
				hms.getParentMessageSource() == null) {
			// Only set parent context as parent MessageSource if no parent MessageSource
			// registered already.
			hms.setParentMessageSource(getInternalParentMessageSource());
		}
	}
	else {
		// Use empty MessageSource to be able to accept getMessage calls.
		DelegatingMessageSource dms = new DelegatingMessageSource();
		dms.setParentMessageSource(getInternalParentMessageSource());
		this.messageSource = dms;
		beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
	}
}

这个方法首先判断容器中是否存在一个名为 messageSource 的 Bean(MESSAGE_SOURCE_BEAN_NAME 常量的实际值就是 messageSource),如果存在,则检查当前容器是否存在 parent,如果存在 parent 容器,那么 parent 容器可能也会有一个 messageSource 对象,就把 parent 容器的 messageSource 对象设置给当前的 messageSource 作为 parentMessageSource。如果当前容器中不存在一个名为 messageSource 的 bean,那么系统就会自动创建一个 DelegatingMessageSource 对象并注册到 Spring 容器中。

从前面的介绍中大家就明白了为什么我们向 Spring 容器中注册 ReloadableResourceBundleMessageSource 的时候,beanName 必须是 messageSource,如果 beanName 不是 messageSource,那么 Spring 容器就会自动创建另外一个 MessageSource 对象了,这就导致最终在获取资源的时候出错。

好啦,这是 MessageSource Bean 加载的方式。加载完成之后,这个 Bean 将来会被初始化,然后我们在需要的时候,调用这个 Bean 中的 getMessage 方法去获取资源,现在我们就去分析 getMessage 方法。

松哥这里的分析就以 ReloadableResourceBundleMessageSource 来展开,因为在整个 MessageSource 体系中,ReloadableResourceBundleMessageSource 是相对比较复杂的一个了,把这个搞懂了,剩下的几个其实都很好懂了。

这个 getMessage 方法实际上是在 ReloadableResourceBundleMessageSource 的父类 AbstractMessageSource 中,换句话说,不同类型的 MessageSource 调用的 getMessage 方法是同一个:

@Override
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
	String msg = getMessageInternal(code, args, locale);
	if (msg != null) {
		return msg;
	}
	if (defaultMessage == null) {
		return getDefaultMessage(code);
	}
	return renderDefaultMessage(defaultMessage, args, locale);
}

这个方法分两步,首先调用 getMessageInternal 尝试去解析出来 key 对应的 value,如果没有找到合适的 value,那么就会使用默认值。

我们先来看 getMessageInternal 方法:

@Nullable
protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
	if (code == null) {
		return null;
	}
	if (locale == null) {
		locale = Locale.getDefault();
	}
	Object[] argsToUse = args;
	if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
		String message = resolveCodeWithoutArguments(code, locale);
		if (message != null) {
			return message;
		}
	}
	else {
		argsToUse = resolveArguments(args, locale);
		MessageFormat messageFormat = resolveCode(code, locale);
		if (messageFormat != null) {
			synchronized (messageFormat) {
				return messageFormat.format(argsToUse);
			}
		}
	}
	Properties commonMessages = getCommonMessages();
	if (commonMessages != null) {
		String commonMessage = commonMessages.getProperty(code);
		if (commonMessage != null) {
			return formatMessage(commonMessage, args, locale);
		}
	}
	return getMessageFromParent(code, argsToUse, locale);
}

这个方法的逻辑还是比较简单的,如果传入的 code 为空就直接返回 null,如果传入的 locale 为空,则获取一个默认的 locale,这个默认的 locale 是根据当前操作系统的信息获取到的一个环境。

接下来,如果不想使用 MessageFormat 并且也没有传入 MessageFormat 所需要的参数,那么就调用 resolveCodeWithoutArguments 方法去解析获取到 Message 对象。如果是需要用到 MessageFormat 对象,那么就调用 resolveCode 方法先去获取到一个 MessageFormat,然后格式化数据并返回。

如果前面两个都没能返回,那么就获取到一个公共的资源,然后尝试去解析 code,如果公共资源也还是没能解析到,那么就去 parent 中尝试解析。

这里涉及到的几个方法,我们分别来看。

@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
	if (getCacheMillis() < 0) {
		PropertiesHolder propHolder = getMergedProperties(locale);
		String result = propHolder.getProperty(code);
		if (result != null) {
			return result;
		}
	}
	else {
		for (String basename : getBasenameSet()) {
			List<String> filenames = calculateAllFilenames(basename, locale);
			for (String filename : filenames) {
				PropertiesHolder propHolder = getProperties(filename);
				String result = propHolder.getProperty(code);
				if (result != null) {
					return result;
				}
			}
		}
	}
	return null;
}

resolveCodeWithoutArguments 方法在 ReloadableResourceBundleMessageSource 类中被重写过了,所以这里我们直接看重写后的方法。

首先会去判断缓存时间是否小于 0,小于 0 表示不缓存,那么就去现场加载数据,否则就从缓存中读取数据。如果是现场加载数据的话,那么就是根据传入的 locale 对象调用 getMergedProperties 方法,获取到 PropertiesHolder 对象,这个对象中封装了读取到的资源文件以及资源文件的时间戳(通过这个时间戳可以判断资源文件是否被修改过)。

getMergedProperties 方法的源码我这里就不贴出来了,就大概和大家说一下大致的流程:首先会根据传入的 basename 和 locale 定位出文件名,会定义出来多种文件名,例如传入的 basename 是 content,locale 是 zh_CN,那么最终生成的可能文件名有五种,如下:

  • content_zh_CN
  • content_zh
  • content_en_CN
  • content_en
  • content

这五种,前两个是根据传入的参数生成的,接下来两个是根据当前系统信息生成的文件名,最后一个则是默认的文件名,接下来就会根据这五个不同的文件名尝试去加载配置文件的,加载配置文件的时候是倒着来的,就是先去查找 content.properties 文件,找到了,就把找到的数据存入到一个 Properties 中,然后继续找上一个,上一个文件要是存在,则将之也存入到 properties 配置文件中,这样,如果有重复的 key,后者就会覆盖掉前者,换言之,上面这个文件名列表中,第一个文件名的优先级是最高的,因为它里边的 key 如果跟前面的 key 重复了,会覆盖掉前面的 key。

这就是 getMergedProperties 方法的大致逻辑。最后就从这个方法的返回值中,找到我们需要的数据返回。

这是不缓存的情况,如果缓存的话,那么就去缓存中读取数据并返回。

大家看去缓存中读取数据的时候,首先也是调用 calculateAllFilenames 方法获取到所有可能的文件名(获取到的结果就是上面列出来的),然后根据文件名去获取数据,这次获取是顺序获取的,即先去查找 content_zh_CN 这个文件,存在的话就直接返回了,这也显示了上面的列表中,从上往下优先级依次降低。然后遍历文件名,调用 getProperties 方法获取对应的 properties 文件,这个获取的过程中,会去检查文件的时间戳,检查资源文件是否被修改过,如果被修改过就重新读取,否则就使用之前已经读取到的缓存数据。

以上就是 resolveCodeWithoutArguments 方法的大概逻辑。

resolveCode 方法的逻辑实际上和 resolveCodeWithoutArguments 类似,唯一的区别在于,resolveCodeWithoutArguments 方法中,存储数据的 Properties 实际上就是我们的资源文件,而在 resolveCode 方法中,存储数据的是一个双层 Map,外层 Map key 是 code,即传入的资源的 key,value 则是一个 Map,里边这个 Map 的 key 是 locale 对象,value 则是一个 MessageFormat 对象,查找的时候根据用户传入的 code 先找到一个 Map,然后再根据用户传入的 locale 找到 MessageFormat,然后返回。其他逻辑基本上都是一致的了。

3.附录

搜刮了一个语言简称表,分享给各位小伙伴:

语言简称
简体中文(中国)zh_CN
繁体中文(中国台湾)zh_TW
繁体中文(中国香港)zh_HK
英语(中国香港)en_HK
英语(美国)en_US
英语(英国)en_GB
英语(全球)en_WW
英语(加拿大)en_CA
英语(澳大利亚)en_AU
英语(爱尔兰)en_IE
英语(芬兰)en_FI
芬兰语(芬兰)fi_FI
英语(丹麦)en_DK
丹麦语(丹麦)da_DK
英语(以色列)en_IL
希伯来语(以色列)he_IL
英语(南非)en_ZA
英语(印度)en_IN
英语(挪威)en_NO
英语(新加坡)en_SG
英语(新西兰)en_NZ
英语(印度尼西亚)en_ID
英语(菲律宾)en_PH
英语(泰国)en_TH
英语(马来西亚)en_MY
英语(阿拉伯)en_XA
韩文(韩国)ko_KR
日语(日本)ja_JP
荷兰语(荷兰)nl_NL
荷兰语(比利时)nl_BE
葡萄牙语(葡萄牙)pt_PT
葡萄牙语(巴西)pt_BR
法语(法国)fr_FR
法语(卢森堡)fr_LU
法语(瑞士)fr_CH
法语(比利时)fr_BE
法语(加拿大)fr_CA
西班牙语(拉丁美洲)es_LA
西班牙语(西班牙)es_ES
西班牙语(阿根廷)es_AR
西班牙语(美国)es_US
西班牙语(墨西哥)es_MX
西班牙语(哥伦比亚)es_CO
西班牙语(波多黎各)es_PR
德语(德国)de_DE
德语(奥地利)de_AT
德语(瑞士)de_CH
俄语(俄罗斯)ru_RU
意大利语(意大利)it_IT
希腊语(希腊)el_GR
挪威语(挪威)no_NO
匈牙利语(匈牙利)hu_HU
土耳其语(土耳其)tr_TR
捷克语(捷克共和国)cs_CZ
斯洛文尼亚语sl_SL
波兰语(波兰)pl_PL
瑞典语(瑞典)sv_SE
西班牙语(智利)es_CL

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/180189.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Faster R-CNN源码解析(一)

目录 前言训练脚本(train_mobilenetv2.py)自定义数据集(my_dataset.py) 前言 Faster R-CNN 是经典的two-stage目标检测模型&#xff0c; 原理上并不是很复杂&#xff0c;也就是RPNFast R-CNN&#xff0c;但是在代码的实现上确实有很多细节&#xff0c;并且源码也非常的多&…

面试题:Java 对象不使用时,为什么要赋值 null ?

文章目录 前言示例代码运行时栈典型的运行时栈Java的栈优化提醒 GC一瞥提醒 JVM的“BUG”总结 前言 最近&#xff0c;许多Java开发者都在讨论说&#xff0c;“不使用的对象应手动赋值为null“ 这句话&#xff0c;而且好多开发者一直信奉着这句话&#xff1b;问其原因&#xff…

专注短视频账号矩阵系统源头开发---saas工具

专注短视频账号矩阵系统源头开发---saas营销化工具&#xff0c;目前我们作为一家纯技术开发团队目前已经专注打磨开发这套系统企业版/线下版两个版本的saas营销拓客工具已经3年了&#xff0c;本套系统逻辑主要是从ai智能批量剪辑、账号矩阵全托管发布、私信触单收录、文案ai智能…

曲率半径的推导

参考文章 参考文章

5V升8.4V升压双节充电芯片WT4059

5V升8.4V升压双节充电芯片WT4059 今天给大家带来一款强大且实用的锂电池充电芯片&#xff1a;WT4059。 WT4059采用同步架构支持双节串联锂电池同步升压充电&#xff0c;它可用外部电阻配置充电电流&#xff0c;使其在应用时仅需极少的外围器件&#xff0c;有效减少整体方案尺寸…

JSP EL表达式获取list/Map集合与java Bean对象

上文 JSP EL表达式基本使用 中 我们对EL表达式做了一个基本的了解 也做了基础的字符串数据使用 那么 我们可以来看一下我们的集合 首先 list 这个比较简单 我们直接这样写代码 <% page import"java.util.ArrayList" %> <% page import"java.util.Lis…

【论文精读】HuggingGPT: Solving AI Tasks with ChatGPT and its Friends in Hugging Face

HuggingGPT: Solving AI Tasks with ChatGPT and its Friends in Hugging Face 前言Abstract1 Introduction2 Related Works3 HuggingGPT3.1 Task PlanningSpecification-based InstructionDemonstration-based Parsing 3.2 Model SelectionIn-context Task-model Assignment 3…

Dubbo框架

1&#xff1a;简介 Dubbo 是阿里巴巴公司开源的一个Java高性能优秀的服务框架 Apache Dubbo 是一款 RPC 服务开发框架&#xff0c;用于解决微服务架构下的服务治理与通信问题 这是Dubbo官网的介绍&#xff0c;下面是我对这dubbo的理解 首先介绍下什么是RPC&#xff1a; 常…

重生奇迹mu迹辅助什么好

主流辅助一号选手&#xff1a;弓箭手 智弓作为最老、最有资历的辅助职业&#xff0c;一直都是各类玩家的首要选择。因为智力MM提供的辅助能力都是最基础、最有效、最直观的辅助。能够减少玩家对于装备的渴求度&#xff0c;直接提升人物的攻防&#xff0c;大大降低了玩家升级打…

python数据结构与算法-14_树与二叉树

树和二叉树 前面我们讲了两种使用分治和递归解决排序问题的归并排序和快速排序&#xff0c;堆排序先就此打住&#xff0c;因为涉及到树的概念&#xff0c;所以我们先来讲讲树。 讲完了树之后后面我们开始介绍一种有用的数据结构堆(heap)&#xff0c; 以及借助堆来实现的堆排序…

豪华程度堪比飞机头等舱?奔驰在北美发布Tourrider系列巴士

今年三月&#xff0c;奔驰工厂附近出现了一台特殊的测试车。其突出的前保险杠以及竖置双风挡等特殊配置&#xff0c;都在暗示着它并非是为欧洲市场打造。 根据特征推测&#xff0c;这台车应该是为北美市场打造。 就在昨天&#xff0c;奔驰发布了旗下全新Tourrider系列豪华客车&…

Unity中颜色空间Gamma与Linear

文章目录 前言一、人眼对光照的自适应1、光照强度与人眼所见的关系2、巧合的是&#xff0c;早期的电子脉冲显示屏也符合这条曲线3、这两条曲线都巧合的符合 y x^2.2^&#xff08;Gamma2.2空间&#xff09; 二、Gamma矫正1、没矫正前&#xff0c;人眼看电子脉冲显示屏&#xff…

二叉搜索树再升级——红黑树

二叉搜索树再升级——红黑树 红黑树的概念红黑树的插入uncle为granfather的右孩子uncle结点为红色uncle结点为空或黑色 uncle为granfather的左孩子 红黑树的概念 之前我们学习了AVL树&#xff0c;这是一种判断左右子树高度来严格控制总体树的高度的树。条件相对来说比较苛刻。…

黑马点评笔记 redis缓存三大问题解决

文章目录 缓存问题缓存穿透问题的解决思路编码解决商品查询的缓存穿透问题 缓存雪崩问题及解决思路缓存击穿问题及解决思路问题分析使用锁来解决代码实现 逻辑过期方案代码实现 缓存问题 我们熟知的是用到缓存就会遇到缓存三大问题&#xff1a; 缓存穿透缓存击穿缓存雪崩 接…

Kafka 常用功能总结(不断更新中....)

kafka 用途 业务中我们经常用来两个方面 1.发送消息 2.发送日志记录 kafka 结构组成 broker&#xff1a;可以理解成一个单独的服务器&#xff0c;所有的东西都归属到broker中 partation&#xff1a;为了增加并发度而做的拆分&#xff0c;相当于把broker拆分成不同的小块&…

基于Python实现汽车销售数据可视化+预测【500010086.1】

导入模块 import numpy as np import pandas as pd from pylab import mpl import plotly.express as px import matplotlib.pyplot as plt import seaborn as sns设置全局字体 plt.rcParams[font.sans-serif][kaiti]获取数据 total_sales_df pd.read_excel(r"./data/中…

从根到叶:随机森林模型的深入探索

一、说明 在本综合指南中&#xff0c;我们将超越基础知识。当您盯着随机森林模型的文档时&#xff0c;您将不再对“节点杂质”、“加权分数”或“成本复杂性修剪”等术语感到不知所措。相反&#xff0c;我们将剖析每个参数&#xff0c;阐明其作用和影响。通过理论和 Python 实践…

【STM32外设系列】GPS定位模块(ATGM336H)

&#x1f380; 文章作者&#xff1a;二土电子 &#x1f338; 关注公众号获取更多资料&#xff01; &#x1f438; 期待大家一起学习交流&#xff01; 文章目录 一、GPS模块简介二、使用方法2.1 引脚介绍2.2 数据帧介绍2.3 关于不同的启动方式 三、前置知识3.1 strstr函数3.2…

【洛谷算法题】P5714-肥胖问题【入门2分支结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5714-肥胖问题【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格式&a…

实在智能携“TARS大模型”入选“2023中国数据智能产业AI大模型先锋企业”

近日&#xff0c;由数据猿与上海大数据联盟联合主办的“2023企业数智化转型升级发展论坛”在上海圆满收官。 论坛颁奖典礼上&#xff0c;《2023中国数据智能产业AI大模型先锋企业》等六大榜单正式揭晓&#xff0c;旨在表彰在AI领域为数智化升级取得卓越成就和突出贡献的企业&am…